diff options
Diffstat (limited to 'core/java/android')
593 files changed, 47457 insertions, 14907 deletions
diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java index 1e3d5be..2620c44 100644 --- a/core/java/android/accessibilityservice/AccessibilityService.java +++ b/core/java/android/accessibilityservice/AccessibilityService.java @@ -23,14 +23,18 @@ import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.RemoteException; +import android.os.SystemClock; import android.util.Log; import android.view.KeyEvent; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityInteractionClient; import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityWindowInfo; import com.android.internal.os.HandlerCaller; +import java.util.List; + /** * An accessibility service runs in the background and receives callbacks by the system * when {@link AccessibilityEvent}s are fired. Such events denote some state transition @@ -180,28 +184,37 @@ import com.android.internal.os.HandlerCaller; * event generation has settled down.</p> * <h3>Event types</h3> * <ul> - * <li>{@link AccessibilityEvent#TYPE_VIEW_CLICKED} - * <li>{@link AccessibilityEvent#TYPE_VIEW_LONG_CLICKED} - * <li>{@link AccessibilityEvent#TYPE_VIEW_FOCUSED} - * <li>{@link AccessibilityEvent#TYPE_VIEW_SELECTED} - * <li>{@link AccessibilityEvent#TYPE_VIEW_TEXT_CHANGED} - * <li>{@link AccessibilityEvent#TYPE_WINDOW_STATE_CHANGED} - * <li>{@link AccessibilityEvent#TYPE_NOTIFICATION_STATE_CHANGED} - * <li>{@link AccessibilityEvent#TYPE_TOUCH_EXPLORATION_GESTURE_START} - * <li>{@link AccessibilityEvent#TYPE_TOUCH_EXPLORATION_GESTURE_END} - * <li>{@link AccessibilityEvent#TYPE_VIEW_HOVER_ENTER} - * <li>{@link AccessibilityEvent#TYPE_VIEW_HOVER_EXIT} - * <li>{@link AccessibilityEvent#TYPE_VIEW_SCROLLED} - * <li>{@link AccessibilityEvent#TYPE_VIEW_TEXT_SELECTION_CHANGED} - * <li>{@link AccessibilityEvent#TYPE_WINDOW_CONTENT_CHANGED} + * <li>{@link AccessibilityEvent#TYPE_VIEW_CLICKED}</li> + * <li>{@link AccessibilityEvent#TYPE_VIEW_LONG_CLICKED}</li> + * <li>{@link AccessibilityEvent#TYPE_VIEW_FOCUSED}</li> + * <li>{@link AccessibilityEvent#TYPE_VIEW_SELECTED}</li> + * <li>{@link AccessibilityEvent#TYPE_VIEW_TEXT_CHANGED}</li> + * <li>{@link AccessibilityEvent#TYPE_WINDOW_STATE_CHANGED}</li> + * <li>{@link AccessibilityEvent#TYPE_NOTIFICATION_STATE_CHANGED}</li> + * <li>{@link AccessibilityEvent#TYPE_TOUCH_EXPLORATION_GESTURE_START}</li> + * <li>{@link AccessibilityEvent#TYPE_TOUCH_EXPLORATION_GESTURE_END}</li> + * <li>{@link AccessibilityEvent#TYPE_VIEW_HOVER_ENTER}</li> + * <li>{@link AccessibilityEvent#TYPE_VIEW_HOVER_EXIT}</li> + * <li>{@link AccessibilityEvent#TYPE_VIEW_SCROLLED}</li> + * <li>{@link AccessibilityEvent#TYPE_VIEW_TEXT_SELECTION_CHANGED}</li> + * <li>{@link AccessibilityEvent#TYPE_WINDOW_CONTENT_CHANGED}</li> + * <li>{@link AccessibilityEvent#TYPE_ANNOUNCEMENT}</li> + * <li>{@link AccessibilityEvent#TYPE_GESTURE_DETECTION_START}</li> + * <li>{@link AccessibilityEvent#TYPE_GESTURE_DETECTION_END}</li> + * <li>{@link AccessibilityEvent#TYPE_TOUCH_INTERACTION_START}</li> + * <li>{@link AccessibilityEvent#TYPE_TOUCH_INTERACTION_END}</li> + * <li>{@link AccessibilityEvent#TYPE_VIEW_ACCESSIBILITY_FOCUSED}</li> + * <li>{@link AccessibilityEvent#TYPE_WINDOWS_CHANGED}</li> + * <li>{@link AccessibilityEvent#TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED}</li> * </ul> * <h3>Feedback types</h3> * <ul> - * <li>{@link AccessibilityServiceInfo#FEEDBACK_AUDIBLE} - * <li>{@link AccessibilityServiceInfo#FEEDBACK_HAPTIC} - * <li>{@link AccessibilityServiceInfo#FEEDBACK_AUDIBLE} - * <li>{@link AccessibilityServiceInfo#FEEDBACK_VISUAL} - * <li>{@link AccessibilityServiceInfo#FEEDBACK_GENERIC} + * <li>{@link AccessibilityServiceInfo#FEEDBACK_AUDIBLE}</li> + * <li>{@link AccessibilityServiceInfo#FEEDBACK_HAPTIC}</li> + * <li>{@link AccessibilityServiceInfo#FEEDBACK_AUDIBLE}</li> + * <li>{@link AccessibilityServiceInfo#FEEDBACK_VISUAL}</li> + * <li>{@link AccessibilityServiceInfo#FEEDBACK_GENERIC}</li> + * <li>{@link AccessibilityServiceInfo#FEEDBACK_BRAILLE}</li> * </ul> * @see AccessibilityEvent * @see AccessibilityServiceInfo @@ -443,8 +456,41 @@ public abstract class AccessibilityService extends Service { } /** + * Gets the windows on the screen. This method returns only the windows + * that a sighted user can interact with, as opposed to all windows. + * For example, if there is a modal dialog shown and the user cannot touch + * anything behind it, then only the modal window will be reported + * (assuming it is the top one). For convenience the returned windows + * are ordered in a descending layer order, which is the windows that + * are higher in the Z-order are reported first. + * <p> + * <strong>Note:</strong> In order to access the windows your service has + * to declare the capability to retrieve window content by setting the + * {@link android.R.styleable#AccessibilityService_canRetrieveWindowContent} + * property in its meta-data. For details refer to {@link #SERVICE_META_DATA}. + * Also the service has to opt-in to retrieve the interactive windows by + * setting the {@link AccessibilityServiceInfo#FLAG_RETRIEVE_INTERACTIVE_WINDOWS} + * flag. + * </p> + * + * @return The windows if there are windows and the service is can retrieve + * them, otherwise an empty list. + */ + public List<AccessibilityWindowInfo> getWindows() { + return AccessibilityInteractionClient.getInstance().getWindows(mConnectionId); + } + + /** * Gets the root node in the currently active window if this service - * can retrieve window content. + * can retrieve window content. The active window is the one that the user + * is currently touching or the window with input focus, if the user is not + * touching any window. + * <p> + * <strong>Note:</strong> In order to access the root node your service has + * to declare the capability to retrieve window content by setting the + * {@link android.R.styleable#AccessibilityService_canRetrieveWindowContent} + * property in its meta-data. For details refer to {@link #SERVICE_META_DATA}. + * </p> * * @return The root node if this service can retrieve window content. */ @@ -480,6 +526,31 @@ public abstract class AccessibilityService extends Service { } /** + * Find the view that has the specified focus type. The search is performed + * across all windows. + * <p> + * <strong>Note:</strong> In order to access the windows your service has + * to declare the capability to retrieve window content by setting the + * {@link android.R.styleable#AccessibilityService_canRetrieveWindowContent} + * property in its meta-data. For details refer to {@link #SERVICE_META_DATA}. + * Also the service has to opt-in to retrieve the interactive windows by + * setting the {@link AccessibilityServiceInfo#FLAG_RETRIEVE_INTERACTIVE_WINDOWS} + * flag.Otherwise, the search will be performed only in the active window. + * </p> + * + * @param focus The focus to find. One of {@link AccessibilityNodeInfo#FOCUS_INPUT} or + * {@link AccessibilityNodeInfo#FOCUS_ACCESSIBILITY}. + * @return The node info of the focused view or null. + * + * @see AccessibilityNodeInfo#FOCUS_INPUT + * @see AccessibilityNodeInfo#FOCUS_ACCESSIBILITY + */ + public AccessibilityNodeInfo findFocus(int focus) { + return AccessibilityInteractionClient.getInstance().findFocus(mConnectionId, + AccessibilityNodeInfo.ANY_WINDOW_ID, AccessibilityNodeInfo.ROOT_NODE_ID, focus); + } + + /** * Gets the an {@link AccessibilityServiceInfo} describing this * {@link AccessibilityService}. This method is useful if one wants * to change some of the dynamically configurable properties at @@ -584,12 +655,13 @@ public abstract class AccessibilityService extends Service { static final int NO_ID = -1; - private static final int DO_SET_SET_CONNECTION = 10; - private static final int DO_ON_INTERRUPT = 20; - private static final int DO_ON_ACCESSIBILITY_EVENT = 30; - private static final int DO_ON_GESTURE = 40; - private static final int DO_CLEAR_ACCESSIBILITY_NODE_INFO_CACHE = 50; - private static final int DO_ON_KEY_EVENT = 60; + private static final int DO_SET_SET_CONNECTION = 1; + private static final int DO_ON_INTERRUPT = 2; + private static final int DO_ON_ACCESSIBILITY_EVENT = 3; + private static final int DO_ON_GESTURE = 4; + private static final int DO_CLEAR_ACCESSIBILITY_CACHE = 5; + private static final int DO_ON_KEY_EVENT = 6; + private static final int DO_ON_WINDOWS_CHANGED = 7; private final HandlerCaller mCaller; @@ -624,8 +696,8 @@ public abstract class AccessibilityService extends Service { mCaller.sendMessage(message); } - public void clearAccessibilityNodeInfoCache() { - Message message = mCaller.obtainMessage(DO_CLEAR_ACCESSIBILITY_NODE_INFO_CACHE); + public void clearAccessibilityCache() { + Message message = mCaller.obtainMessage(DO_CLEAR_ACCESSIBILITY_CACHE); mCaller.sendMessage(message); } @@ -635,6 +707,13 @@ public abstract class AccessibilityService extends Service { mCaller.sendMessage(message); } + @Override + public void onWindowsChanged(int[] windowIds) { + Message message = mCaller.obtainMessageO(DO_ON_WINDOWS_CHANGED, windowIds); + mCaller.sendMessage(message); + } + + @Override public void executeMessage(Message message) { switch (message.what) { case DO_ON_ACCESSIBILITY_EVENT: { @@ -642,12 +721,19 @@ public abstract class AccessibilityService extends Service { if (event != null) { AccessibilityInteractionClient.getInstance().onAccessibilityEvent(event); mCallback.onAccessibilityEvent(event); - event.recycle(); + // Make sure the event is recycled. + try { + event.recycle(); + } catch (IllegalStateException ise) { + /* ignore - best effort */ + } } } return; + case DO_ON_INTERRUPT: { mCallback.onInterrupt(); } return; + case DO_SET_SET_CONNECTION: { mConnectionId = message.arg1; IAccessibilityServiceConnection connection = @@ -658,18 +744,22 @@ public abstract class AccessibilityService extends Service { mCallback.onSetConnectionId(mConnectionId); mCallback.onServiceConnected(); } else { - AccessibilityInteractionClient.getInstance().removeConnection(mConnectionId); + AccessibilityInteractionClient.getInstance().removeConnection( + mConnectionId); AccessibilityInteractionClient.getInstance().clearCache(); mCallback.onSetConnectionId(AccessibilityInteractionClient.NO_ID); } } return; + case DO_ON_GESTURE: { final int gestureId = message.arg1; mCallback.onGesture(gestureId); } return; - case DO_CLEAR_ACCESSIBILITY_NODE_INFO_CACHE: { + + case DO_CLEAR_ACCESSIBILITY_CACHE: { AccessibilityInteractionClient.getInstance().clearCache(); } return; + case DO_ON_KEY_EVENT: { KeyEvent event = (KeyEvent) message.obj; try { @@ -685,9 +775,40 @@ public abstract class AccessibilityService extends Service { } } } finally { - event.recycle(); + // Make sure the event is recycled. + try { + event.recycle(); + } catch (IllegalStateException ise) { + /* ignore - best effort */ + } } } return; + + case DO_ON_WINDOWS_CHANGED: { + final int[] windowIds = (int[]) message.obj; + + // Update the cached windows first. + // NOTE: The cache will hold on to the windows so do not recycle. + if (windowIds != null) { + AccessibilityInteractionClient.getInstance().removeWindows(windowIds); + } + + // Let the client know the windows changed. + AccessibilityEvent event = AccessibilityEvent.obtain( + AccessibilityEvent.TYPE_WINDOWS_CHANGED); + event.setEventTime(SystemClock.uptimeMillis()); + event.setSealed(true); + + mCallback.onAccessibilityEvent(event); + + // Make sure the event is recycled. + try { + event.recycle(); + } catch (IllegalStateException ise) { + /* ignore - best effort */ + } + } break; + default : Log.w(LOG_TAG, "Unknown message type " + message.what); } diff --git a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java index bdc4fdd..4f9ba59 100644 --- a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java +++ b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java @@ -284,6 +284,27 @@ public class AccessibilityServiceInfo implements Parcelable { public static final int FLAG_REQUEST_FILTER_KEY_EVENTS = 0x00000020; /** + * This flag indicates to the system that the accessibility service wants + * to access content of all interactive windows. An interactive window is a + * window that can be touched by a sighted user when explore by touch is not + * enabled. If this flag is not set your service will not receive + * {@link android.view.accessibility.AccessibilityEvent#TYPE_WINDOWS_CHANGED} + * events, calling AccessibilityService{@link AccessibilityService#getWindows() + * AccessibilityService.getWindows()} will return an empty list, and {@link + * AccessibilityNodeInfo#getWindow() AccessibilityNodeInfo.getWindow()} will + * return null. + * <p> + * Services that want to set this flag have to declare the capability + * to retrieve window content in their meta-data by setting the attribute + * {@link android.R.attr#canRetrieveWindowContent canRetrieveWindowContent} to + * true, otherwise this flag will be ignored. For how to declare the meta-data + * of a service refer to {@value AccessibilityService#SERVICE_META_DATA}. + * </p> + * @see android.R.styleable#AccessibilityService_canRetrieveWindowContent + */ + public static final int FLAG_RETRIEVE_INTERACTIVE_WINDOWS = 0x00000040; + + /** * The event types an {@link AccessibilityService} is interested in. * <p> * <strong>Can be dynamically set at runtime.</strong> @@ -302,6 +323,15 @@ public class AccessibilityServiceInfo implements Parcelable { * @see android.view.accessibility.AccessibilityEvent#TYPE_VIEW_SCROLLED * @see android.view.accessibility.AccessibilityEvent#TYPE_VIEW_TEXT_SELECTION_CHANGED * @see android.view.accessibility.AccessibilityEvent#TYPE_WINDOW_CONTENT_CHANGED + * @see android.view.accessibility.AccessibilityEvent#TYPE_TOUCH_INTERACTION_START + * @see android.view.accessibility.AccessibilityEvent#TYPE_TOUCH_INTERACTION_END + * @see android.view.accessibility.AccessibilityEvent#TYPE_ANNOUNCEMENT + * @see android.view.accessibility.AccessibilityEvent#TYPE_GESTURE_DETECTION_START + * @see android.view.accessibility.AccessibilityEvent#TYPE_GESTURE_DETECTION_END + * @see android.view.accessibility.AccessibilityEvent#TYPE_VIEW_ACCESSIBILITY_FOCUSED + * @see android.view.accessibility.AccessibilityEvent#TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED + * @see android.view.accessibility.AccessibilityEvent#TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY + * @see android.view.accessibility.AccessibilityEvent#TYPE_WINDOWS_CHANGED */ public int eventTypes; @@ -354,6 +384,7 @@ public class AccessibilityServiceInfo implements Parcelable { * @see #FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY * @see #FLAG_REQUEST_FILTER_KEY_EVENTS * @see #FLAG_REPORT_VIEW_IDS + * @see #FLAG_RETRIEVE_INTERACTIVE_WINDOWS */ public int flags; @@ -449,7 +480,7 @@ public class AccessibilityServiceInfo implements Parcelable { com.android.internal.R.styleable.AccessibilityService_accessibilityFeedbackType, 0); notificationTimeout = asAttributes.getInt( - com.android.internal.R.styleable.AccessibilityService_notificationTimeout, + com.android.internal.R.styleable.AccessibilityService_notificationTimeout, 0); flags = asAttributes.getInt( com.android.internal.R.styleable.AccessibilityService_accessibilityFlags, 0); @@ -861,6 +892,8 @@ public class AccessibilityServiceInfo implements Parcelable { return "FLAG_REPORT_VIEW_IDS"; case FLAG_REQUEST_FILTER_KEY_EVENTS: return "FLAG_REQUEST_FILTER_KEY_EVENTS"; + case FLAG_RETRIEVE_INTERACTIVE_WINDOWS: + return "FLAG_RETRIEVE_INTERACTIVE_WINDOWS"; default: return null; } diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl index c5e3d43a..edd8727 100644 --- a/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl +++ b/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl @@ -18,6 +18,7 @@ package android.accessibilityservice; import android.accessibilityservice.IAccessibilityServiceConnection; import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityWindowInfo; import android.view.KeyEvent; /** @@ -35,7 +36,9 @@ import android.view.KeyEvent; void onGesture(int gesture); - void clearAccessibilityNodeInfoCache(); + void clearAccessibilityCache(); void onKeyEvent(in KeyEvent event, int sequence); + + void onWindowsChanged(in int[] changedWindowIds); } diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl index 3df06b5..5f7a17d 100644 --- a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl +++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl @@ -21,6 +21,7 @@ import android.accessibilityservice.AccessibilityServiceInfo; import android.view.MagnificationSpec; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.IAccessibilityInteractionConnectionCallback; +import android.view.accessibility.AccessibilityWindowInfo; /** * Interface given to an AccessibilitySerivce to talk to the AccessibilityManagerService. @@ -53,6 +54,10 @@ interface IAccessibilityServiceConnection { int action, in Bundle arguments, int interactionId, IAccessibilityInteractionConnectionCallback callback, long threadId); + AccessibilityWindowInfo getWindow(int windowId); + + List<AccessibilityWindowInfo> getWindows(); + AccessibilityServiceInfo getServiceInfo(); boolean performGlobalAction(int action); diff --git a/core/java/android/accounts/AccountAuthenticatorActivity.java b/core/java/android/accounts/AccountAuthenticatorActivity.java index 6a55ddf..f9284e6 100644 --- a/core/java/android/accounts/AccountAuthenticatorActivity.java +++ b/core/java/android/accounts/AccountAuthenticatorActivity.java @@ -17,7 +17,6 @@ package android.accounts; import android.app.Activity; -import android.content.Intent; import android.os.Bundle; /** diff --git a/core/java/android/accounts/AccountManagerFuture.java b/core/java/android/accounts/AccountManagerFuture.java index a1ab00c..af00a08 100644 --- a/core/java/android/accounts/AccountManagerFuture.java +++ b/core/java/android/accounts/AccountManagerFuture.java @@ -15,10 +15,7 @@ */ package android.accounts; -import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeoutException; import java.io.IOException; /** diff --git a/core/java/android/accounts/ChooseAccountActivity.java b/core/java/android/accounts/ChooseAccountActivity.java index bfbae24..242b3ea 100644 --- a/core/java/android/accounts/ChooseAccountActivity.java +++ b/core/java/android/accounts/ChooseAccountActivity.java @@ -100,7 +100,7 @@ public class ChooseAccountActivity extends Activity { try { AuthenticatorDescription desc = mTypeToAuthDescription.get(accountType); Context authContext = createPackageContext(desc.packageName, 0); - icon = authContext.getResources().getDrawable(desc.iconId); + icon = authContext.getDrawable(desc.iconId); } catch (PackageManager.NameNotFoundException e) { // Nothing we can do much here, just log if (Log.isLoggable(TAG, Log.WARN)) { diff --git a/core/java/android/accounts/ChooseAccountTypeActivity.java b/core/java/android/accounts/ChooseAccountTypeActivity.java index acc8549..a3222d8 100644 --- a/core/java/android/accounts/ChooseAccountTypeActivity.java +++ b/core/java/android/accounts/ChooseAccountTypeActivity.java @@ -129,7 +129,7 @@ public class ChooseAccountTypeActivity extends Activity { Drawable icon = null; try { Context authContext = createPackageContext(desc.packageName, 0); - icon = authContext.getResources().getDrawable(desc.iconId); + icon = authContext.getDrawable(desc.iconId); final CharSequence sequence = authContext.getResources().getText(desc.labelId); if (sequence != null) { name = sequence.toString(); diff --git a/core/java/android/accounts/GrantCredentialsPermissionActivity.java b/core/java/android/accounts/GrantCredentialsPermissionActivity.java index 8b01c6a..12b2b9c 100644 --- a/core/java/android/accounts/GrantCredentialsPermissionActivity.java +++ b/core/java/android/accounts/GrantCredentialsPermissionActivity.java @@ -16,7 +16,6 @@ package android.accounts; import android.app.Activity; -import android.content.pm.RegisteredServicesCache; import android.content.res.Resources; import android.os.Bundle; import android.widget.TextView; @@ -30,7 +29,6 @@ import android.text.TextUtils; import com.android.internal.R; import java.io.IOException; -import java.net.Authenticator; /** * @hide diff --git a/core/java/android/alsa/AlsaCardsParser.java b/core/java/android/alsa/AlsaCardsParser.java new file mode 100644 index 0000000..f9af979 --- /dev/null +++ b/core/java/android/alsa/AlsaCardsParser.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.alsascan; + +import android.util.Slog; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.util.Vector; + +/** + * @hide Retrieves information from an ALSA "cards" file. + */ +public class AlsaCardsParser { + private static final String TAG = "AlsaCardsParser"; + + private static LineTokenizer tokenizer_ = new LineTokenizer(" :[]"); + + public class AlsaCardRecord { + public int mCardNum = -1; + public String mField1 = ""; + public String mCardName = ""; + public String mCardDescription = ""; + + public AlsaCardRecord() {} + + public boolean parse(String line, int lineIndex) { + int tokenIndex = 0; + int delimIndex = 0; + if (lineIndex == 0) { + // line # (skip) + tokenIndex = tokenizer_.nextToken(line, tokenIndex); + delimIndex = tokenizer_.nextDelimiter(line, tokenIndex); + + // mField1 + tokenIndex = tokenizer_.nextToken(line, delimIndex); + delimIndex = tokenizer_.nextDelimiter(line, tokenIndex); + mField1 = line.substring(tokenIndex, delimIndex); + + // mCardName + tokenIndex = tokenizer_.nextToken(line, delimIndex); + // delimIndex = tokenizer_.nextDelimiter(line, tokenIndex); + mCardName = line.substring(tokenIndex); + // done + } else if (lineIndex == 1) { + tokenIndex = tokenizer_.nextToken(line, 0); + if (tokenIndex != -1) { + mCardDescription = line.substring(tokenIndex); + } + } + + return true; + } + + public String textFormat() { + return mCardName + " : " + mCardDescription; + } + } + + private Vector<AlsaCardRecord> cardRecords_ = new Vector<AlsaCardRecord>(); + + public void scan() { + cardRecords_.clear(); + final String cardsFilePath = "/proc/asound/cards"; + File cardsFile = new File(cardsFilePath); + try { + FileReader reader = new FileReader(cardsFile); + BufferedReader bufferedReader = new BufferedReader(reader); + String line = ""; + while ((line = bufferedReader.readLine()) != null) { + AlsaCardRecord cardRecord = new AlsaCardRecord(); + cardRecord.parse(line, 0); + cardRecord.parse(line = bufferedReader.readLine(), 1); + cardRecords_.add(cardRecord); + } + reader.close(); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public AlsaCardRecord getCardRecordAt(int index) { + return cardRecords_.get(index); + } + + public int getNumCardRecords() { + return cardRecords_.size(); + } + + public void Log() { + int numCardRecs = getNumCardRecords(); + for (int index = 0; index < numCardRecs; ++index) { + Slog.w(TAG, "usb:" + getCardRecordAt(index).textFormat()); + } + } + + public AlsaCardsParser() {} +} diff --git a/core/java/android/alsa/AlsaDevicesParser.java b/core/java/android/alsa/AlsaDevicesParser.java new file mode 100644 index 0000000..094c8a2 --- /dev/null +++ b/core/java/android/alsa/AlsaDevicesParser.java @@ -0,0 +1,275 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.alsascan; + +import android.util.Slog; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.util.Vector; + +/** + * @hide + * Retrieves information from an ALSA "devices" file. + */ +public class AlsaDevicesParser { + private static final String TAG = "AlsaDevicesParser"; + + private static final int kIndex_CardDeviceField = 5; + private static final int kStartIndex_CardNum = 6; + private static final int kEndIndex_CardNum = 8; // one past + private static final int kStartIndex_DeviceNum = 9; + private static final int kEndIndex_DeviceNum = 11; // one past + private static final int kStartIndex_Type = 14; + + private static LineTokenizer mTokenizer = new LineTokenizer(" :[]-"); + + private boolean mHasCaptureDevices = false; + private boolean mHasPlaybackDevices = false; + private boolean mHasMIDIDevices = false; + + public class AlsaDeviceRecord { + public static final int kDeviceType_Unknown = -1; + public static final int kDeviceType_Audio = 0; + public static final int kDeviceType_Control = 1; + public static final int kDeviceType_MIDI = 2; + + public static final int kDeviceDir_Unknown = -1; + public static final int kDeviceDir_Capture = 0; + public static final int kDeviceDir_Playback = 1; + + int mCardNum = -1; + int mDeviceNum = -1; + int mDeviceType = kDeviceType_Unknown; + int mDeviceDir = kDeviceDir_Unknown; + + public AlsaDeviceRecord() { + } + + public boolean parse(String line) { + // "0123456789012345678901234567890" + // " 2: [ 0-31]: digital audio playback" + // " 3: [ 0-30]: digital audio capture" + // " 35: [ 1] : control" + // " 36: [ 2- 0]: raw midi" + + final int kToken_LineNum = 0; + final int kToken_CardNum = 1; + final int kToken_DeviceNum = 2; + final int kToken_Type0 = 3; // "digital", "control", "raw" + final int kToken_Type1 = 4; // "audio", "midi" + final int kToken_Type2 = 5; // "capture", "playback" + + int tokenOffset = 0; + int delimOffset = 0; + int tokenIndex = kToken_LineNum; + while (true) { + tokenOffset = mTokenizer.nextToken(line, delimOffset); + if (tokenOffset == LineTokenizer.kTokenNotFound) { + break; // bail + } + delimOffset = mTokenizer.nextDelimiter(line, tokenOffset); + if (delimOffset == LineTokenizer.kTokenNotFound) { + delimOffset = line.length(); + } + String token = line.substring(tokenOffset, delimOffset); + + switch (tokenIndex) { + case kToken_LineNum: + // ignore + break; + + case kToken_CardNum: + mCardNum = Integer.parseInt(token); + if (line.charAt(delimOffset) != '-') { + tokenIndex++; // no device # in the token stream + } + break; + + case kToken_DeviceNum: + mDeviceNum = Integer.parseInt(token); + break; + + case kToken_Type0: + if (token.equals("digital")) { + // NOP + } else if (token.equals("control")) { + mDeviceType = kDeviceType_Control; + } else if (token.equals("raw")) { + // NOP + } + break; + + case kToken_Type1: + if (token.equals("audio")) { + mDeviceType = kDeviceType_Audio; + } else if (token.equals("midi")) { + mDeviceType = kDeviceType_MIDI; + mHasMIDIDevices = true; + } + break; + + case kToken_Type2: + if (token.equals("capture")) { + mDeviceDir = kDeviceDir_Capture; + mHasCaptureDevices = true; + } else if (token.equals("playback")) { + mDeviceDir = kDeviceDir_Playback; + mHasPlaybackDevices = true; + } + break; + } // switch (tokenIndex) + + tokenIndex++; + } // while (true) + + return true; + } // parse() + + public String textFormat() { + StringBuilder sb = new StringBuilder(); + sb.append("[" + mCardNum + ":" + mDeviceNum + "]"); + + switch (mDeviceType) { + case kDeviceType_Unknown: + sb.append(" N/A"); + break; + case kDeviceType_Audio: + sb.append(" Audio"); + break; + case kDeviceType_Control: + sb.append(" Control"); + break; + case kDeviceType_MIDI: + sb.append(" MIDI"); + break; + } + + switch (mDeviceDir) { + case kDeviceDir_Unknown: + sb.append(" N/A"); + break; + case kDeviceDir_Capture: + sb.append(" Capture"); + break; + case kDeviceDir_Playback: + sb.append(" Playback"); + break; + } + + return sb.toString(); + } + } + + private Vector<AlsaDeviceRecord> + deviceRecords_ = new Vector<AlsaDeviceRecord>(); + + private boolean isLineDeviceRecord(String line) { + return line.charAt(kIndex_CardDeviceField) == '['; + } + + public AlsaDevicesParser() { + } + + public int getNumDeviceRecords() { + return deviceRecords_.size(); + } + + public AlsaDeviceRecord getDeviceRecordAt(int index) { + return deviceRecords_.get(index); + } + + public void Log() { + int numDevRecs = getNumDeviceRecords(); + for (int index = 0; index < numDevRecs; ++index) { + Slog.w(TAG, "usb:" + getDeviceRecordAt(index).textFormat()); + } + } + + public boolean hasPlaybackDevices() { + return mHasPlaybackDevices; + } + + public boolean hasPlaybackDevices(int card) { + for (int index = 0; index < deviceRecords_.size(); index++) { + AlsaDeviceRecord deviceRecord = deviceRecords_.get(index); + if (deviceRecord.mCardNum == card && + deviceRecord.mDeviceType == AlsaDeviceRecord.kDeviceType_Audio && + deviceRecord.mDeviceDir == AlsaDeviceRecord.kDeviceDir_Playback) { + return true; + } + } + return false; + } + + public boolean hasCaptureDevices() { + return mHasCaptureDevices; + } + + public boolean hasCaptureDevices(int card) { + for (int index = 0; index < deviceRecords_.size(); index++) { + AlsaDeviceRecord deviceRecord = deviceRecords_.get(index); + if (deviceRecord.mCardNum == card && + deviceRecord.mDeviceType == AlsaDeviceRecord.kDeviceType_Audio && + deviceRecord.mDeviceDir == AlsaDeviceRecord.kDeviceDir_Capture) { + return true; + } + } + return false; + } + + public boolean hasMIDIDevices() { + return mHasMIDIDevices; + } + + public boolean hasMIDIDevices(int card) { + for (int index = 0; index < deviceRecords_.size(); index++) { + AlsaDeviceRecord deviceRecord = deviceRecords_.get(index); + if (deviceRecord.mCardNum == card && + deviceRecord.mDeviceType == AlsaDeviceRecord.kDeviceType_MIDI) { + return true; + } + } + return false; + } + + public void scan() { + deviceRecords_.clear(); + + final String devicesFilePath = "/proc/asound/devices"; + File devicesFile = new File(devicesFilePath); + try { + FileReader reader = new FileReader(devicesFile); + BufferedReader bufferedReader = new BufferedReader(reader); + String line = ""; + while ((line = bufferedReader.readLine()) != null) { + if (isLineDeviceRecord(line)) { + AlsaDeviceRecord deviceRecord = new AlsaDeviceRecord(); + deviceRecord.parse(line); + deviceRecords_.add(deviceRecord); + } + } + reader.close(); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } +} // class AlsaDevicesParser + diff --git a/core/java/android/alsa/LineTokenizer.java b/core/java/android/alsa/LineTokenizer.java new file mode 100644 index 0000000..c138fc5 --- /dev/null +++ b/core/java/android/alsa/LineTokenizer.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.alsascan; + +/** + * @hide + * Breaks lines in an ALSA "cards" or "devices" file into tokens. + * TODO(pmclean) Look into replacing this with String.split(). + */ +public class LineTokenizer { + public static final int kTokenNotFound = -1; + + private String mDelimiters = ""; + + public LineTokenizer(String delimiters) { + mDelimiters = delimiters; + } + + int nextToken(String line, int startIndex) { + int len = line.length(); + int offset = startIndex; + for (; offset < len; offset++) { + if (mDelimiters.indexOf(line.charAt(offset)) == -1) { + // past a delimiter + break; + } + } + + return offset < len ? offset : kTokenNotFound; + } + + int nextDelimiter(String line, int startIndex) { + int len = line.length(); + int offset = startIndex; + for (; offset < len; offset++) { + if (mDelimiters.indexOf(line.charAt(offset)) != -1) { + // past a delimiter + break; + } + } + + return offset < len ? offset : kTokenNotFound; + } +} diff --git a/core/java/android/animation/AnimatorInflater.java b/core/java/android/animation/AnimatorInflater.java index d753e32..20236aa 100644 --- a/core/java/android/animation/AnimatorInflater.java +++ b/core/java/android/animation/AnimatorInflater.java @@ -215,7 +215,7 @@ public class AnimatorInflater { (toType <= TypedValue.TYPE_LAST_COLOR_INT))) { // special case for colors: ignore valueType and get ints getFloats = false; - anim.setEvaluator(new ArgbEvaluator()); + evaluator = ArgbEvaluator.getInstance(); } if (getFloats) { diff --git a/core/java/android/animation/ArgbEvaluator.java b/core/java/android/animation/ArgbEvaluator.java index 717a3d9..ed07195 100644 --- a/core/java/android/animation/ArgbEvaluator.java +++ b/core/java/android/animation/ArgbEvaluator.java @@ -21,6 +21,19 @@ package android.animation; * values that represent ARGB colors. */ public class ArgbEvaluator implements TypeEvaluator { + private static final ArgbEvaluator sInstance = new ArgbEvaluator(); + + /** + * Returns an instance of <code>ArgbEvaluator</code> that may be used in + * {@link ValueAnimator#setEvaluator(TypeEvaluator)}. The same instance may + * be used in multiple <code>Animator</code>s because it holds no state. + * @return An instance of <code>ArgbEvalutor</code>. + * + * @hide + */ + public static ArgbEvaluator getInstance() { + return sInstance; + } /** * This function returns the calculated in-between value for a color diff --git a/core/java/android/animation/FloatArrayEvaluator.java b/core/java/android/animation/FloatArrayEvaluator.java new file mode 100644 index 0000000..9ae1197 --- /dev/null +++ b/core/java/android/animation/FloatArrayEvaluator.java @@ -0,0 +1,77 @@ +/* + * 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.animation; + +/** + * This evaluator can be used to perform type interpolation between <code>float[]</code> values. + * Each index into the array is treated as a separate value to interpolate. For example, + * evaluating <code>{100, 200}</code> and <code>{300, 400}</code> will interpolate the value at + * the first index between 100 and 300 and the value at the second index value between 200 and 400. + */ +public class FloatArrayEvaluator implements TypeEvaluator<float[]> { + + private float[] mArray; + + /** + * Create a FloatArrayEvaluator that does not reuse the animated value. Care must be taken + * when using this option because on every evaluation a new <code>float[]</code> will be + * allocated. + * + * @see #FloatArrayEvaluator(float[]) + */ + public FloatArrayEvaluator() { + } + + /** + * Create a FloatArrayEvaluator that reuses <code>reuseArray</code> for every evaluate() call. + * Caution must be taken to ensure that the value returned from + * {@link android.animation.ValueAnimator#getAnimatedValue()} is not cached, modified, or + * used across threads. The value will be modified on each <code>evaluate()</code> call. + * + * @param reuseArray The array to modify and return from <code>evaluate</code>. + */ + public FloatArrayEvaluator(float[] reuseArray) { + mArray = reuseArray; + } + + /** + * Interpolates the value at each index by the fraction. If + * {@link #FloatArrayEvaluator(float[])} was used to construct this object, + * <code>reuseArray</code> will be returned, otherwise a new <code>float[]</code> + * will be returned. + * + * @param fraction The fraction from the starting to the ending values + * @param startValue The start value. + * @param endValue The end value. + * @return A <code>float[]</code> where each element is an interpolation between + * the same index in startValue and endValue. + */ + @Override + public float[] evaluate(float fraction, float[] startValue, float[] endValue) { + float[] array = mArray; + if (array == null) { + array = new float[startValue.length]; + } + + for (int i = 0; i < array.length; i++) { + float start = startValue[i]; + float end = endValue[i]; + array[i] = start + (fraction * (end - start)); + } + return array; + } +} diff --git a/core/java/android/animation/IntArrayEvaluator.java b/core/java/android/animation/IntArrayEvaluator.java new file mode 100644 index 0000000..d7f10f3 --- /dev/null +++ b/core/java/android/animation/IntArrayEvaluator.java @@ -0,0 +1,75 @@ +/* + * 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.animation; + +/** + * This evaluator can be used to perform type interpolation between <code>int[]</code> values. + * Each index into the array is treated as a separate value to interpolate. For example, + * evaluating <code>{100, 200}</code> and <code>{300, 400}</code> will interpolate the value at + * the first index between 100 and 300 and the value at the second index value between 200 and 400. + */ +public class IntArrayEvaluator implements TypeEvaluator<int[]> { + + private int[] mArray; + + /** + * Create an IntArrayEvaluator that does not reuse the animated value. Care must be taken + * when using this option because on every evaluation a new <code>int[]</code> will be + * allocated. + * + * @see #IntArrayEvaluator(int[]) + */ + public IntArrayEvaluator() { + } + + /** + * Create an IntArrayEvaluator that reuses <code>reuseArray</code> for every evaluate() call. + * Caution must be taken to ensure that the value returned from + * {@link android.animation.ValueAnimator#getAnimatedValue()} is not cached, modified, or + * used across threads. The value will be modified on each <code>evaluate()</code> call. + * + * @param reuseArray The array to modify and return from <code>evaluate</code>. + */ + public IntArrayEvaluator(int[] reuseArray) { + mArray = reuseArray; + } + + /** + * Interpolates the value at each index by the fraction. If {@link #IntArrayEvaluator(int[])} + * was used to construct this object, <code>reuseArray</code> will be returned, otherwise + * a new <code>int[]</code> will be returned. + * + * @param fraction The fraction from the starting to the ending values + * @param startValue The start value. + * @param endValue The end value. + * @return An <code>int[]</code> where each element is an interpolation between + * the same index in startValue and endValue. + */ + @Override + public int[] evaluate(float fraction, int[] startValue, int[] endValue) { + int[] array = mArray; + if (array == null) { + array = new int[startValue.length]; + } + for (int i = 0; i < array.length; i++) { + int start = startValue[i]; + int end = endValue[i]; + array[i] = (int) (start + (fraction * (end - start))); + } + return array; + } +} diff --git a/core/java/android/animation/ObjectAnimator.java b/core/java/android/animation/ObjectAnimator.java index 9c88ccf..c0ce795 100644 --- a/core/java/android/animation/ObjectAnimator.java +++ b/core/java/android/animation/ObjectAnimator.java @@ -16,6 +16,8 @@ package android.animation; +import android.graphics.Path; +import android.graphics.PointF; import android.util.Log; import android.util.Property; @@ -191,7 +193,7 @@ public final class ObjectAnimator extends ValueAnimator { /** * Constructs and returns an ObjectAnimator that animates between int values. A single - * value implies that that value is the one being animated to. Two values imply a starting + * value implies that that value is the one being animated to. Two values imply starting * and ending values. More than two values imply a starting value, values to animate through * along the way, and an ending value (these values will be distributed evenly across * the duration of the animation). @@ -210,8 +212,33 @@ public final class ObjectAnimator extends ValueAnimator { } /** + * Constructs and returns an ObjectAnimator that animates coordinates along a <code>Path</code> + * using two properties. A <code>Path</code></> animation moves in two dimensions, animating + * coordinates <code>(x, y)</code> together to follow the line. In this variation, the + * coordinates are integers that are set to separate properties designated by + * <code>xPropertyName</code> and <code>yPropertyName</code>. + * + * @param target The object whose properties are to be animated. This object should + * have public methods on it called <code>setNameX()</code> and + * <code>setNameY</code>, where <code>nameX</code> and <code>nameY</code> + * are the value of <code>xPropertyName</code> and <code>yPropertyName</code> + * parameters, respectively. + * @param xPropertyName The name of the property for the x coordinate being animated. + * @param yPropertyName The name of the property for the y coordinate being animated. + * @param path The <code>Path</code> to animate values along. + * @return An ObjectAnimator object that is set up to animate along <code>path</code>. + */ + public static ObjectAnimator ofInt(Object target, String xPropertyName, String yPropertyName, + Path path) { + Keyframe[][] keyframes = PropertyValuesHolder.createKeyframes(path, true); + PropertyValuesHolder x = PropertyValuesHolder.ofKeyframe(xPropertyName, keyframes[0]); + PropertyValuesHolder y = PropertyValuesHolder.ofKeyframe(yPropertyName, keyframes[1]); + return ofPropertyValuesHolder(target, x, y); + } + + /** * Constructs and returns an ObjectAnimator that animates between int values. A single - * value implies that that value is the one being animated to. Two values imply a starting + * value implies that that value is the one being animated to. Two values imply starting * and ending values. More than two values imply a starting value, values to animate through * along the way, and an ending value (these values will be distributed evenly across * the duration of the animation). @@ -228,8 +255,135 @@ public final class ObjectAnimator extends ValueAnimator { } /** + * Constructs and returns an ObjectAnimator that animates coordinates along a <code>Path</code> + * using two properties. A <code>Path</code></> animation moves in two dimensions, animating + * coordinates <code>(x, y)</code> together to follow the line. In this variation, the + * coordinates are integers that are set to separate properties, <code>xProperty</code> and + * <code>yProperty</code>. + * + * @param target The object whose properties are to be animated. + * @param xProperty The property for the x coordinate being animated. + * @param yProperty The property for the y coordinate being animated. + * @param path The <code>Path</code> to animate values along. + * @return An ObjectAnimator object that is set up to animate along <code>path</code>. + */ + public static <T> ObjectAnimator ofInt(T target, Property<T, Integer> xProperty, + Property<T, Integer> yProperty, Path path) { + Keyframe[][] keyframes = PropertyValuesHolder.createKeyframes(path, true); + PropertyValuesHolder x = PropertyValuesHolder.ofKeyframe(xProperty, keyframes[0]); + PropertyValuesHolder y = PropertyValuesHolder.ofKeyframe(yProperty, keyframes[1]); + return ofPropertyValuesHolder(target, x, y); + } + + /** + * Constructs and returns an ObjectAnimator that animates over int values for a multiple + * parameters setter. Only public methods that take only int parameters are supported. + * Each <code>int[]</code> contains a complete set of parameters to the setter method. + * At least two <code>int[]</code> values must be provided, a start and end. More than two + * values imply a starting value, values to animate through along the way, and an ending + * value (these values will be distributed evenly across the duration of the animation). + * + * @param target The object whose property is to be animated. This object may + * have a public method on it called <code>setName()</code>, where <code>name</code> is + * the value of the <code>propertyName</code> parameter. <code>propertyName</code> may also + * be the case-sensitive complete name of the public setter method. + * @param propertyName The name of the property being animated or the name of the setter method. + * @param values A set of values that the animation will animate between over time. + * @return An ObjectAnimator object that is set up to animate between the given values. + */ + public static ObjectAnimator ofMultiInt(Object target, String propertyName, int[][] values) { + PropertyValuesHolder pvh = PropertyValuesHolder.ofMultiInt(propertyName, values); + return ofPropertyValuesHolder(target, pvh); + } + + /** + * Constructs and returns an ObjectAnimator that animates the target using a multi-int setter + * along the given <code>Path</code>. A <code>Path</code></> animation moves in two dimensions, + * animating coordinates <code>(x, y)</code> together to follow the line. In this variation, the + * coordinates are integer x and y coordinates used in the first and second parameter of the + * setter, respectively. + * + * @param target The object whose property is to be animated. This object may + * have a public method on it called <code>setName()</code>, where <code>name</code> is + * the value of the <code>propertyName</code> parameter. <code>propertyName</code> may also + * be the case-sensitive complete name of the public setter method. + * @param propertyName The name of the property being animated or the name of the setter method. + * @param path The <code>Path</code> to animate values along. + * @return An ObjectAnimator object that is set up to animate along <code>path</code>. + */ + public static ObjectAnimator ofMultiInt(Object target, String propertyName, Path path) { + PropertyValuesHolder pvh = PropertyValuesHolder.ofMultiInt(propertyName, path); + return ofPropertyValuesHolder(target, pvh); + } + + /** + * Constructs and returns an ObjectAnimator that animates over values for a multiple int + * parameters setter. Only public methods that take only int parameters are supported. + * <p>At least two values must be provided, a start and end. More than two + * values imply a starting value, values to animate through along the way, and an ending + * value (these values will be distributed evenly across the duration of the animation).</p> + * + * @param target The object whose property is to be animated. This object may + * have a public method on it called <code>setName()</code>, where <code>name</code> is + * the value of the <code>propertyName</code> parameter. <code>propertyName</code> may also + * be the case-sensitive complete name of the public setter method. + * @param propertyName The name of the property being animated or the name of the setter method. + * @param converter Converts T objects into int parameters for the multi-value setter. + * @param evaluator A TypeEvaluator that will be called on each animation frame to + * provide the necessary interpolation between the Object values to derive the animated + * value. + * @param values A set of values that the animation will animate between over time. + * @return An ObjectAnimator object that is set up to animate between the given values. + */ + public static <T> ObjectAnimator ofMultiInt(Object target, String propertyName, + TypeConverter<T, int[]> converter, TypeEvaluator<T> evaluator, T... values) { + PropertyValuesHolder pvh = PropertyValuesHolder.ofMultiInt(propertyName, converter, + evaluator, values); + return ObjectAnimator.ofPropertyValuesHolder(target, pvh); + } + + /** + * Constructs and returns an ObjectAnimator that animates between color values. A single + * value implies that that value is the one being animated to. Two values imply starting + * and ending values. More than two values imply a starting value, values to animate through + * along the way, and an ending value (these values will be distributed evenly across + * the duration of the animation). + * + * @param target The object whose property is to be animated. This object should + * have a public method on it called <code>setName()</code>, where <code>name</code> is + * the value of the <code>propertyName</code> parameter. + * @param propertyName The name of the property being animated. + * @param values A set of values that the animation will animate between over time. + * @return An ObjectAnimator object that is set up to animate between the given values. + */ + public static ObjectAnimator ofArgb(Object target, String propertyName, int... values) { + ObjectAnimator animator = ofInt(target, propertyName, values); + animator.setEvaluator(ArgbEvaluator.getInstance()); + return animator; + } + + /** + * Constructs and returns an ObjectAnimator that animates between color values. A single + * value implies that that value is the one being animated to. Two values imply starting + * and ending values. More than two values imply a starting value, values to animate through + * along the way, and an ending value (these values will be distributed evenly across + * the duration of the animation). + * + * @param target The object whose property is to be animated. + * @param property The property being animated. + * @param values A set of values that the animation will animate between over time. + * @return An ObjectAnimator object that is set up to animate between the given values. + */ + public static <T> ObjectAnimator ofArgb(T target, Property<T, Integer> property, + int... values) { + ObjectAnimator animator = ofInt(target, property, values); + animator.setEvaluator(ArgbEvaluator.getInstance()); + return animator; + } + + /** * Constructs and returns an ObjectAnimator that animates between float values. A single - * value implies that that value is the one being animated to. Two values imply a starting + * value implies that that value is the one being animated to. Two values imply starting * and ending values. More than two values imply a starting value, values to animate through * along the way, and an ending value (these values will be distributed evenly across * the duration of the animation). @@ -248,8 +402,33 @@ public final class ObjectAnimator extends ValueAnimator { } /** + * Constructs and returns an ObjectAnimator that animates coordinates along a <code>Path</code> + * using two properties. A <code>Path</code></> animation moves in two dimensions, animating + * coordinates <code>(x, y)</code> together to follow the line. In this variation, the + * coordinates are floats that are set to separate properties designated by + * <code>xPropertyName</code> and <code>yPropertyName</code>. + * + * @param target The object whose properties are to be animated. This object should + * have public methods on it called <code>setNameX()</code> and + * <code>setNameY</code>, where <code>nameX</code> and <code>nameY</code> + * are the value of the <code>xPropertyName</code> and <code>yPropertyName</code> + * parameters, respectively. + * @param xPropertyName The name of the property for the x coordinate being animated. + * @param yPropertyName The name of the property for the y coordinate being animated. + * @param path The <code>Path</code> to animate values along. + * @return An ObjectAnimator object that is set up to animate along <code>path</code>. + */ + public static ObjectAnimator ofFloat(Object target, String xPropertyName, String yPropertyName, + Path path) { + Keyframe[][] keyframes = PropertyValuesHolder.createKeyframes(path, false); + PropertyValuesHolder x = PropertyValuesHolder.ofKeyframe(xPropertyName, keyframes[0]); + PropertyValuesHolder y = PropertyValuesHolder.ofKeyframe(yPropertyName, keyframes[1]); + return ofPropertyValuesHolder(target, x, y); + } + + /** * Constructs and returns an ObjectAnimator that animates between float values. A single - * value implies that that value is the one being animated to. Two values imply a starting + * value implies that that value is the one being animated to. Two values imply starting * and ending values. More than two values imply a starting value, values to animate through * along the way, and an ending value (these values will be distributed evenly across * the duration of the animation). @@ -267,8 +446,94 @@ public final class ObjectAnimator extends ValueAnimator { } /** + * Constructs and returns an ObjectAnimator that animates coordinates along a <code>Path</code> + * using two properties. A <code>Path</code></> animation moves in two dimensions, animating + * coordinates <code>(x, y)</code> together to follow the line. In this variation, the + * coordinates are floats that are set to separate properties, <code>xProperty</code> and + * <code>yProperty</code>. + * + * @param target The object whose properties are to be animated. + * @param xProperty The property for the x coordinate being animated. + * @param yProperty The property for the y coordinate being animated. + * @param path The <code>Path</code> to animate values along. + * @return An ObjectAnimator object that is set up to animate along <code>path</code>. + */ + public static <T> ObjectAnimator ofFloat(T target, Property<T, Float> xProperty, + Property<T, Float> yProperty, Path path) { + return ofFloat(target, xProperty.getName(), yProperty.getName(), path); + } + + /** + * Constructs and returns an ObjectAnimator that animates over float values for a multiple + * parameters setter. Only public methods that take only float parameters are supported. + * Each <code>float[]</code> contains a complete set of parameters to the setter method. + * At least two <code>float[]</code> values must be provided, a start and end. More than two + * values imply a starting value, values to animate through along the way, and an ending + * value (these values will be distributed evenly across the duration of the animation). + * + * @param target The object whose property is to be animated. This object may + * have a public method on it called <code>setName()</code>, where <code>name</code> is + * the value of the <code>propertyName</code> parameter. <code>propertyName</code> may also + * be the case-sensitive complete name of the public setter method. + * @param propertyName The name of the property being animated or the name of the setter method. + * @param values A set of values that the animation will animate between over time. + * @return An ObjectAnimator object that is set up to animate between the given values. + */ + public static ObjectAnimator ofMultiFloat(Object target, String propertyName, + float[][] values) { + PropertyValuesHolder pvh = PropertyValuesHolder.ofMultiFloat(propertyName, values); + return ofPropertyValuesHolder(target, pvh); + } + + /** + * Constructs and returns an ObjectAnimator that animates the target using a multi-float setter + * along the given <code>Path</code>. A <code>Path</code></> animation moves in two dimensions, + * animating coordinates <code>(x, y)</code> together to follow the line. In this variation, the + * coordinates are float x and y coordinates used in the first and second parameter of the + * setter, respectively. + * + * @param target The object whose property is to be animated. This object may + * have a public method on it called <code>setName()</code>, where <code>name</code> is + * the value of the <code>propertyName</code> parameter. <code>propertyName</code> may also + * be the case-sensitive complete name of the public setter method. + * @param propertyName The name of the property being animated or the name of the setter method. + * @param path The <code>Path</code> to animate values along. + * @return An ObjectAnimator object that is set up to animate along <code>path</code>. + */ + public static ObjectAnimator ofMultiFloat(Object target, String propertyName, Path path) { + PropertyValuesHolder pvh = PropertyValuesHolder.ofMultiFloat(propertyName, path); + return ofPropertyValuesHolder(target, pvh); + } + + /** + * Constructs and returns an ObjectAnimator that animates over values for a multiple float + * parameters setter. Only public methods that take only float parameters are supported. + * <p>At least two values must be provided, a start and end. More than two + * values imply a starting value, values to animate through along the way, and an ending + * value (these values will be distributed evenly across the duration of the animation).</p> + * + * @param target The object whose property is to be animated. This object may + * have a public method on it called <code>setName()</code>, where <code>name</code> is + * the value of the <code>propertyName</code> parameter. <code>propertyName</code> may also + * be the case-sensitive complete name of the public setter method. + * @param propertyName The name of the property being animated or the name of the setter method. + * @param converter Converts T objects into float parameters for the multi-value setter. + * @param evaluator A TypeEvaluator that will be called on each animation frame to + * provide the necessary interpolation between the Object values to derive the animated + * value. + * @param values A set of values that the animation will animate between over time. + * @return An ObjectAnimator object that is set up to animate between the given values. + */ + public static <T> ObjectAnimator ofMultiFloat(Object target, String propertyName, + TypeConverter<T, float[]> converter, TypeEvaluator<T> evaluator, T... values) { + PropertyValuesHolder pvh = PropertyValuesHolder.ofMultiFloat(propertyName, converter, + evaluator, values); + return ObjectAnimator.ofPropertyValuesHolder(target, pvh); + } + + /** * Constructs and returns an ObjectAnimator that animates between Object values. A single - * value implies that that value is the one being animated to. Two values imply a starting + * value implies that that value is the one being animated to. Two values imply starting * and ending values. More than two values imply a starting value, values to animate through * along the way, and an ending value (these values will be distributed evenly across * the duration of the animation). @@ -292,8 +557,32 @@ public final class ObjectAnimator extends ValueAnimator { } /** + * Constructs and returns an ObjectAnimator that animates a property along a <code>Path</code>. + * A <code>Path</code></> animation moves in two dimensions, animating coordinates + * <code>(x, y)</code> together to follow the line. This variant animates the coordinates + * in a <code>PointF</code> to follow the <code>Path</code>. If the <code>Property</code> + * associated with <code>propertyName</code> uses a type other than <code>PointF</code>, + * <code>converter</code> can be used to change from <code>PointF</code> to the type + * associated with the <code>Property</code>. + * + * @param target The object whose property is to be animated. This object should + * have a public method on it called <code>setName()</code>, where <code>name</code> is + * the value of the <code>propertyName</code> parameter. + * @param propertyName The name of the property being animated. + * @param converter Converts a PointF to the type associated with the setter. May be + * null if conversion is unnecessary. + * @param path The <code>Path</code> to animate values along. + * @return An ObjectAnimator object that is set up to animate along <code>path</code>. + */ + public static ObjectAnimator ofObject(Object target, String propertyName, + TypeConverter<PointF, ?> converter, Path path) { + PropertyValuesHolder pvh = PropertyValuesHolder.ofObject(propertyName, converter, path); + return ofPropertyValuesHolder(target, pvh); + } + + /** * Constructs and returns an ObjectAnimator that animates between Object values. A single - * value implies that that value is the one being animated to. Two values imply a starting + * value implies that that value is the one being animated to. Two values imply starting * and ending values. More than two values imply a starting value, values to animate through * along the way, and an ending value (these values will be distributed evenly across * the duration of the animation). @@ -315,6 +604,53 @@ public final class ObjectAnimator extends ValueAnimator { } /** + * Constructs and returns an ObjectAnimator that animates between Object values. A single + * value implies that that value is the one being animated to. Two values imply starting + * and ending values. More than two values imply a starting value, values to animate through + * along the way, and an ending value (these values will be distributed evenly across + * the duration of the animation). This variant supplies a <code>TypeConverter</code> to + * convert from the animated values to the type of the property. If only one value is + * supplied, the <code>TypeConverter</code> must implement + * {@link TypeConverter#convertBack(Object)} to retrieve the current value. + * + * @param target The object whose property is to be animated. + * @param property The property being animated. + * @param converter Converts the animated object to the Property type. + * @param evaluator A TypeEvaluator that will be called on each animation frame to + * provide the necessary interpolation between the Object values to derive the animated + * value. + * @param values A set of values that the animation will animate between over time. + * @return An ObjectAnimator object that is set up to animate between the given values. + */ + public static <T, V, P> ObjectAnimator ofObject(T target, Property<T, P> property, + TypeConverter<V, P> converter, TypeEvaluator<V> evaluator, V... values) { + PropertyValuesHolder pvh = PropertyValuesHolder.ofObject(property, converter, evaluator, + values); + return ofPropertyValuesHolder(target, pvh); + } + + /** + * Constructs and returns an ObjectAnimator that animates a property along a <code>Path</code>. + * A <code>Path</code></> animation moves in two dimensions, animating coordinates + * <code>(x, y)</code> together to follow the line. This variant animates the coordinates + * in a <code>PointF</code> to follow the <code>Path</code>. If <code>property</code> + * uses a type other than <code>PointF</code>, <code>converter</code> can be used to change + * from <code>PointF</code> to the type associated with the <code>Property</code>. + * + * @param target The object whose property is to be animated. + * @param property The property being animated. Should not be null. + * @param converter Converts a PointF to the type associated with the setter. May be + * null if conversion is unnecessary. + * @param path The <code>Path</code> to animate values along. + * @return An ObjectAnimator object that is set up to animate along <code>path</code>. + */ + public static <T, V> ObjectAnimator ofObject(T target, Property<T, V> property, + TypeConverter<PointF, V> converter, Path path) { + PropertyValuesHolder pvh = PropertyValuesHolder.ofObject(property, converter, path); + return ofPropertyValuesHolder(target, pvh); + } + + /** * Constructs and returns an ObjectAnimator that animates between the sets of values specified * in <code>PropertyValueHolder</code> objects. This variant should be used when animating * several properties at once with the same ObjectAnimator, since PropertyValuesHolder allows diff --git a/core/java/android/animation/PointFEvaluator.java b/core/java/android/animation/PointFEvaluator.java new file mode 100644 index 0000000..91d501f --- /dev/null +++ b/core/java/android/animation/PointFEvaluator.java @@ -0,0 +1,83 @@ +/* + * 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.animation; + +import android.graphics.PointF; + +/** + * This evaluator can be used to perform type interpolation between <code>PointF</code> values. + */ +public class PointFEvaluator implements TypeEvaluator<PointF> { + + /** + * When null, a new PointF is returned on every evaluate call. When non-null, + * mPoint will be modified and returned on every evaluate. + */ + private PointF mPoint; + + /** + * Construct a PointFEvaluator that returns a new PointF on every evaluate call. + * To avoid creating an object for each evaluate call, + * {@link PointFEvaluator#PointFEvaluator(android.graphics.PointF)} should be used + * whenever possible. + */ + public PointFEvaluator() { + } + + /** + * Constructs a PointFEvaluator that modifies and returns <code>reuse</code> + * in {@link #evaluate(float, android.graphics.PointF, android.graphics.PointF)} calls. + * The value returned from + * {@link #evaluate(float, android.graphics.PointF, android.graphics.PointF)} should + * not be cached because it will change over time as the object is reused on each + * call. + * + * @param reuse A PointF to be modified and returned by evaluate. + */ + public PointFEvaluator(PointF reuse) { + mPoint = reuse; + } + + /** + * This function returns the result of linearly interpolating the start and + * end PointF values, with <code>fraction</code> representing the proportion + * between the start and end values. The calculation is a simple parametric + * calculation on each of the separate components in the PointF objects + * (x, y). + * + * <p>If {@link #PointFEvaluator(android.graphics.PointF)} was used to construct + * this PointFEvaluator, the object returned will be the <code>reuse</code> + * passed into the constructor.</p> + * + * @param fraction The fraction from the starting to the ending values + * @param startValue The start PointF + * @param endValue The end PointF + * @return A linear interpolation between the start and end values, given the + * <code>fraction</code> parameter. + */ + @Override + public PointF evaluate(float fraction, PointF startValue, PointF endValue) { + float x = startValue.x + (fraction * (endValue.x - startValue.x)); + float y = startValue.y + (fraction * (endValue.y - startValue.y)); + + if (mPoint != null) { + mPoint.set(x, y); + return mPoint; + } else { + return new PointF(x, y); + } + } +} diff --git a/core/java/android/animation/PropertyValuesHolder.java b/core/java/android/animation/PropertyValuesHolder.java index 21f6eda..8fce80a 100644 --- a/core/java/android/animation/PropertyValuesHolder.java +++ b/core/java/android/animation/PropertyValuesHolder.java @@ -16,6 +16,9 @@ package android.animation; +import android.graphics.Path; +import android.graphics.PointF; +import android.util.FloatMath; import android.util.FloatProperty; import android.util.IntProperty; import android.util.Log; @@ -124,6 +127,11 @@ public class PropertyValuesHolder implements Cloneable { private Object mAnimatedValue; /** + * Converts from the source Object type to the setter Object type. + */ + private TypeConverter mConverter; + + /** * Internal utility constructor, used by the factory methods to set the property name. * @param propertyName The name of the property for this holder. */ @@ -166,6 +174,104 @@ public class PropertyValuesHolder implements Cloneable { /** * Constructs and returns a PropertyValuesHolder with a given property name and + * set of <code>int[]</code> values. At least two <code>int[]</code> values must be supplied, + * a start and end value. If more values are supplied, the values will be animated from the + * start, through all intermediate values to the end value. When used with ObjectAnimator, + * the elements of the array represent the parameters of the setter function. + * + * @param propertyName The name of the property being animated. Can also be the + * case-sensitive name of the entire setter method. Should not be null. + * @param values The values that the property will animate between. + * @return PropertyValuesHolder The constructed PropertyValuesHolder object. + * @see IntArrayEvaluator#IntArrayEvaluator(int[]) + * @see ObjectAnimator#ofMultiInt(Object, String, TypeConverter, TypeEvaluator, Object[]) + */ + public static PropertyValuesHolder ofMultiInt(String propertyName, int[][] values) { + if (values.length < 2) { + throw new IllegalArgumentException("At least 2 values must be supplied"); + } + int numParameters = 0; + for (int i = 0; i < values.length; i++) { + if (values[i] == null) { + throw new IllegalArgumentException("values must not be null"); + } + int length = values[i].length; + if (i == 0) { + numParameters = length; + } else if (length != numParameters) { + throw new IllegalArgumentException("Values must all have the same length"); + } + } + IntArrayEvaluator evaluator = new IntArrayEvaluator(new int[numParameters]); + return new MultiIntValuesHolder(propertyName, null, evaluator, (Object[]) values); + } + + /** + * Constructs and returns a PropertyValuesHolder with a given property name to use + * as a multi-int setter. The values are animated along the path, with the first + * parameter of the setter set to the x coordinate and the second set to the y coordinate. + * + * @param propertyName The name of the property being animated. Can also be the + * case-sensitive name of the entire setter method. Should not be null. + * The setter must take exactly two <code>int</code> parameters. + * @param path The Path along which the values should be animated. + * @return PropertyValuesHolder The constructed PropertyValuesHolder object. + * @see ObjectAnimator#ofPropertyValuesHolder(Object, PropertyValuesHolder...) + */ + public static PropertyValuesHolder ofMultiInt(String propertyName, Path path) { + Keyframe[] keyframes = createKeyframes(path); + KeyframeSet keyframeSet = KeyframeSet.ofKeyframe(keyframes); + TypeEvaluator<PointF> evaluator = new PointFEvaluator(new PointF()); + PointFToIntArray converter = new PointFToIntArray(); + return new MultiIntValuesHolder(propertyName, converter, evaluator, keyframeSet); + } + + /** + * Constructs and returns a PropertyValuesHolder with a given property and + * set of Object values for use with ObjectAnimator multi-value setters. The Object + * values are converted to <code>int[]</code> using the converter. + * + * @param propertyName The property being animated or complete name of the setter. + * Should not be null. + * @param converter Used to convert the animated value to setter parameters. + * @param evaluator A TypeEvaluator that will be called on each animation frame to + * provide the necessary interpolation between the Object values to derive the animated + * value. + * @param values The values that the property will animate between. + * @return PropertyValuesHolder The constructed PropertyValuesHolder object. + * @see ObjectAnimator#ofMultiInt(Object, String, TypeConverter, TypeEvaluator, Object[]) + * @see ObjectAnimator#ofPropertyValuesHolder(Object, PropertyValuesHolder...) + */ + public static <V> PropertyValuesHolder ofMultiInt(String propertyName, + TypeConverter<V, int[]> converter, TypeEvaluator<V> evaluator, V... values) { + return new MultiIntValuesHolder(propertyName, converter, evaluator, values); + } + + /** + * Constructs and returns a PropertyValuesHolder object with the specified property name or + * setter name for use in a multi-int setter function using ObjectAnimator. The values can be + * of any type, but the type should be consistent so that the supplied + * {@link android.animation.TypeEvaluator} can be used to to evaluate the animated value. The + * <code>converter</code> converts the values to parameters in the setter function. + * + * <p>At least two values must be supplied, a start and an end value.</p> + * + * @param propertyName The name of the property to associate with the set of values. This + * may also be the complete name of a setter function. + * @param converter Converts <code>values</code> into int parameters for the setter. + * Can be null if the Keyframes have int[] values. + * @param evaluator Used to interpolate between values. + * @param values The values at specific fractional times to evaluate between + * @return A PropertyValuesHolder for a multi-int parameter setter. + */ + public static <T> PropertyValuesHolder ofMultiInt(String propertyName, + TypeConverter<T, int[]> converter, TypeEvaluator<T> evaluator, Keyframe... values) { + KeyframeSet keyframeSet = KeyframeSet.ofKeyframe(values); + return new MultiIntValuesHolder(propertyName, converter, evaluator, keyframeSet); + } + + /** + * Constructs and returns a PropertyValuesHolder with a given property name and * set of float values. * @param propertyName The name of the property being animated. * @param values The values that the named property will animate between. @@ -188,6 +294,103 @@ public class PropertyValuesHolder implements Cloneable { /** * Constructs and returns a PropertyValuesHolder with a given property name and + * set of <code>float[]</code> values. At least two <code>float[]</code> values must be supplied, + * a start and end value. If more values are supplied, the values will be animated from the + * start, through all intermediate values to the end value. When used with ObjectAnimator, + * the elements of the array represent the parameters of the setter function. + * + * @param propertyName The name of the property being animated. Can also be the + * case-sensitive name of the entire setter method. Should not be null. + * @param values The values that the property will animate between. + * @return PropertyValuesHolder The constructed PropertyValuesHolder object. + * @see FloatArrayEvaluator#FloatArrayEvaluator(float[]) + * @see ObjectAnimator#ofMultiFloat(Object, String, TypeConverter, TypeEvaluator, Object[]) + */ + public static PropertyValuesHolder ofMultiFloat(String propertyName, float[][] values) { + if (values.length < 2) { + throw new IllegalArgumentException("At least 2 values must be supplied"); + } + int numParameters = 0; + for (int i = 0; i < values.length; i++) { + if (values[i] == null) { + throw new IllegalArgumentException("values must not be null"); + } + int length = values[i].length; + if (i == 0) { + numParameters = length; + } else if (length != numParameters) { + throw new IllegalArgumentException("Values must all have the same length"); + } + } + FloatArrayEvaluator evaluator = new FloatArrayEvaluator(new float[numParameters]); + return new MultiFloatValuesHolder(propertyName, null, evaluator, (Object[]) values); + } + + /** + * Constructs and returns a PropertyValuesHolder with a given property name to use + * as a multi-float setter. The values are animated along the path, with the first + * parameter of the setter set to the x coordinate and the second set to the y coordinate. + * + * @param propertyName The name of the property being animated. Can also be the + * case-sensitive name of the entire setter method. Should not be null. + * The setter must take exactly two <code>float</code> parameters. + * @param path The Path along which the values should be animated. + * @return PropertyValuesHolder The constructed PropertyValuesHolder object. + * @see ObjectAnimator#ofPropertyValuesHolder(Object, PropertyValuesHolder...) + */ + public static PropertyValuesHolder ofMultiFloat(String propertyName, Path path) { + Keyframe[] keyframes = createKeyframes(path); + KeyframeSet keyframeSet = KeyframeSet.ofKeyframe(keyframes); + TypeEvaluator<PointF> evaluator = new PointFEvaluator(new PointF()); + PointFToFloatArray converter = new PointFToFloatArray(); + return new MultiFloatValuesHolder(propertyName, converter, evaluator, keyframeSet); + } + + /** + * Constructs and returns a PropertyValuesHolder with a given property and + * set of Object values for use with ObjectAnimator multi-value setters. The Object + * values are converted to <code>float[]</code> using the converter. + * + * @param propertyName The property being animated or complete name of the setter. + * Should not be null. + * @param converter Used to convert the animated value to setter parameters. + * @param evaluator A TypeEvaluator that will be called on each animation frame to + * provide the necessary interpolation between the Object values to derive the animated + * value. + * @param values The values that the property will animate between. + * @return PropertyValuesHolder The constructed PropertyValuesHolder object. + * @see ObjectAnimator#ofMultiFloat(Object, String, TypeConverter, TypeEvaluator, Object[]) + */ + public static <V> PropertyValuesHolder ofMultiFloat(String propertyName, + TypeConverter<V, float[]> converter, TypeEvaluator<V> evaluator, V... values) { + return new MultiFloatValuesHolder(propertyName, converter, evaluator, values); + } + + /** + * Constructs and returns a PropertyValuesHolder object with the specified property name or + * setter name for use in a multi-float setter function using ObjectAnimator. The values can be + * of any type, but the type should be consistent so that the supplied + * {@link android.animation.TypeEvaluator} can be used to to evaluate the animated value. The + * <code>converter</code> converts the values to parameters in the setter function. + * + * <p>At least two values must be supplied, a start and an end value.</p> + * + * @param propertyName The name of the property to associate with the set of values. This + * may also be the complete name of a setter function. + * @param converter Converts <code>values</code> into float parameters for the setter. + * Can be null if the Keyframes have float[] values. + * @param evaluator Used to interpolate between values. + * @param values The values at specific fractional times to evaluate between + * @return A PropertyValuesHolder for a multi-float parameter setter. + */ + public static <T> PropertyValuesHolder ofMultiFloat(String propertyName, + TypeConverter<T, float[]> converter, TypeEvaluator<T> evaluator, Keyframe... values) { + KeyframeSet keyframeSet = KeyframeSet.ofKeyframe(values); + return new MultiFloatValuesHolder(propertyName, converter, evaluator, keyframeSet); + } + + /** + * Constructs and returns a PropertyValuesHolder with a given property name and * set of Object values. This variant also takes a TypeEvaluator because the system * cannot automatically interpolate between objects of unknown type. * @@ -207,6 +410,27 @@ public class PropertyValuesHolder implements Cloneable { } /** + * Constructs and returns a PropertyValuesHolder with a given property name and + * a Path along which the values should be animated. This variant supports a + * <code>TypeConverter</code> to convert from <code>PointF</code> to the target + * type. + * + * @param propertyName The name of the property being animated. + * @param converter Converts a PointF to the type associated with the setter. May be + * null if conversion is unnecessary. + * @param path The Path along which the values should be animated. + * @return PropertyValuesHolder The constructed PropertyValuesHolder object. + */ + public static PropertyValuesHolder ofObject(String propertyName, + TypeConverter<PointF, ?> converter, Path path) { + Keyframe[] keyframes = createKeyframes(path); + PropertyValuesHolder pvh = ofKeyframe(propertyName, keyframes); + pvh.setEvaluator(new PointFEvaluator(new PointF())); + pvh.setConverter(converter); + return pvh; + } + + /** * Constructs and returns a PropertyValuesHolder with a given property and * set of Object values. This variant also takes a TypeEvaluator because the system * cannot automatically interpolate between objects of unknown type. @@ -227,6 +451,55 @@ public class PropertyValuesHolder implements Cloneable { } /** + * Constructs and returns a PropertyValuesHolder with a given property and + * set of Object values. This variant also takes a TypeEvaluator because the system + * cannot automatically interpolate between objects of unknown type. This variant also + * takes a <code>TypeConverter</code> to convert from animated values to the type + * of the property. If only one value is supplied, the <code>TypeConverter</code> + * must implement {@link TypeConverter#convertBack(Object)} to retrieve the current + * value. + * + * @param property The property being animated. Should not be null. + * @param converter Converts the animated object to the Property type. + * @param evaluator A TypeEvaluator that will be called on each animation frame to + * provide the necessary interpolation between the Object values to derive the animated + * value. + * @param values The values that the property will animate between. + * @return PropertyValuesHolder The constructed PropertyValuesHolder object. + * @see #setConverter(TypeConverter) + * @see TypeConverter + */ + public static <T, V> PropertyValuesHolder ofObject(Property<?, V> property, + TypeConverter<T, V> converter, TypeEvaluator<T> evaluator, T... values) { + PropertyValuesHolder pvh = new PropertyValuesHolder(property); + pvh.setConverter(converter); + pvh.setObjectValues(values); + pvh.setEvaluator(evaluator); + return pvh; + } + + /** + * Constructs and returns a PropertyValuesHolder with a given property and + * a Path along which the values should be animated. This variant supports a + * <code>TypeConverter</code> to convert from <code>PointF</code> to the target + * type. + * + * @param property The property being animated. Should not be null. + * @param converter Converts a PointF to the type associated with the setter. May be + * null if conversion is unnecessary. + * @param path The Path along which the values should be animated. + * @return PropertyValuesHolder The constructed PropertyValuesHolder object. + */ + public static <V> PropertyValuesHolder ofObject(Property<?, V> property, + TypeConverter<PointF, V> converter, Path path) { + Keyframe[] keyframes = createKeyframes(path); + PropertyValuesHolder pvh = ofKeyframe(property, keyframes); + pvh.setEvaluator(new PointFEvaluator(new PointF())); + pvh.setConverter(converter); + return pvh; + } + + /** * Constructs and returns a PropertyValuesHolder object with the specified property name and set * of values. These values can be of any type, but the type should be consistent so that * an appropriate {@link android.animation.TypeEvaluator} can be found that matches @@ -361,6 +634,14 @@ public class PropertyValuesHolder implements Cloneable { } /** + * Sets the converter to convert from the values type to the setter's parameter type. + * @param converter The converter to use to convert values. + */ + public void setConverter(TypeConverter converter) { + mConverter = converter; + } + + /** * Determine the setter or getter function using the JavaBeans convention of setFoo or * getFoo for a property named 'foo'. This function figures out what the name of the * function should be and uses reflection to find the Method with that name on the @@ -389,22 +670,24 @@ public class PropertyValuesHolder implements Cloneable { } else { args = new Class[1]; Class typeVariants[]; - if (mValueType.equals(Float.class)) { + if (valueType.equals(Float.class)) { typeVariants = FLOAT_VARIANTS; - } else if (mValueType.equals(Integer.class)) { + } else if (valueType.equals(Integer.class)) { typeVariants = INTEGER_VARIANTS; - } else if (mValueType.equals(Double.class)) { + } else if (valueType.equals(Double.class)) { typeVariants = DOUBLE_VARIANTS; } else { typeVariants = new Class[1]; - typeVariants[0] = mValueType; + typeVariants[0] = valueType; } for (Class typeVariant : typeVariants) { args[0] = typeVariant; try { returnVal = targetClass.getMethod(methodName, args); - // change the value type to suit - mValueType = typeVariant; + if (mConverter == null) { + // change the value type to suit + mValueType = typeVariant; + } return returnVal; } catch (NoSuchMethodException e) { // Swallow the error and keep trying other variants @@ -415,7 +698,7 @@ public class PropertyValuesHolder implements Cloneable { if (returnVal == null) { Log.w("PropertyValuesHolder", "Method " + - getMethodName(prefix, mPropertyName) + "() with type " + mValueType + + getMethodName(prefix, mPropertyName) + "() with type " + valueType + " not found on target class " + targetClass); } @@ -465,7 +748,8 @@ public class PropertyValuesHolder implements Cloneable { * @param targetClass The Class on which the requested method should exist. */ void setupSetter(Class targetClass) { - mSetter = setupSetterOrGetter(targetClass, sSetterPropertyMap, "set", mValueType); + Class<?> propertyType = mConverter == null ? mValueType : mConverter.getTargetType(); + mSetter = setupSetterOrGetter(targetClass, sSetterPropertyMap, "set", propertyType); } /** @@ -489,10 +773,13 @@ public class PropertyValuesHolder implements Cloneable { if (mProperty != null) { // check to make sure that mProperty is on the class of target try { - Object testValue = mProperty.get(target); + Object testValue = null; for (Keyframe kf : mKeyframeSet.mKeyframes) { if (!kf.hasValue()) { - kf.setValue(mProperty.get(target)); + if (testValue == null) { + testValue = convertBack(mProperty.get(target)); + } + kf.setValue(testValue); } } return; @@ -516,7 +803,8 @@ public class PropertyValuesHolder implements Cloneable { } } try { - kf.setValue(mGetter.invoke(target)); + Object value = convertBack(mGetter.invoke(target)); + kf.setValue(value); } catch (InvocationTargetException e) { Log.e("PropertyValuesHolder", e.toString()); } catch (IllegalAccessException e) { @@ -526,6 +814,18 @@ public class PropertyValuesHolder implements Cloneable { } } + private Object convertBack(Object value) { + if (mConverter != null) { + value = mConverter.convertBack(value); + if (value == null) { + throw new IllegalArgumentException("Converter " + + mConverter.getClass().getName() + + " must implement convertBack and not return null."); + } + } + return value; + } + /** * Utility function to set the value stored in a particular Keyframe. The value used is * whatever the value is for the property name specified in the keyframe on the target object. @@ -535,7 +835,8 @@ public class PropertyValuesHolder implements Cloneable { */ private void setupValue(Object target, Keyframe kf) { if (mProperty != null) { - kf.setValue(mProperty.get(target)); + Object value = convertBack(mProperty.get(target)); + kf.setValue(value); } try { if (mGetter == null) { @@ -546,7 +847,8 @@ public class PropertyValuesHolder implements Cloneable { return; } } - kf.setValue(mGetter.invoke(target)); + Object value = convertBack(mGetter.invoke(target)); + kf.setValue(value); } catch (InvocationTargetException e) { Log.e("PropertyValuesHolder", e.toString()); } catch (IllegalAccessException e) { @@ -657,7 +959,8 @@ public class PropertyValuesHolder implements Cloneable { * @param fraction The elapsed, interpolated fraction of the animation. */ void calculateValue(float fraction) { - mAnimatedValue = mKeyframeSet.getValue(fraction); + Object value = mKeyframeSet.getValue(fraction); + mAnimatedValue = mConverter == null ? value : mConverter.convert(value); } /** @@ -1015,8 +1318,334 @@ public class PropertyValuesHolder implements Cloneable { } + static class MultiFloatValuesHolder extends PropertyValuesHolder { + private long mJniSetter; + private static final HashMap<Class, HashMap<String, Long>> sJNISetterPropertyMap = + new HashMap<Class, HashMap<String, Long>>(); + + public MultiFloatValuesHolder(String propertyName, TypeConverter converter, + TypeEvaluator evaluator, Object... values) { + super(propertyName); + setConverter(converter); + setObjectValues(values); + setEvaluator(evaluator); + } + + public MultiFloatValuesHolder(String propertyName, TypeConverter converter, + TypeEvaluator evaluator, KeyframeSet keyframeSet) { + super(propertyName); + setConverter(converter); + mKeyframeSet = keyframeSet; + setEvaluator(evaluator); + } + + /** + * Internal function to set the value on the target object, using the setter set up + * earlier on this PropertyValuesHolder object. This function is called by ObjectAnimator + * to handle turning the value calculated by ValueAnimator into a value set on the object + * according to the name of the property. + * + * @param target The target object on which the value is set + */ + @Override + void setAnimatedValue(Object target) { + float[] values = (float[]) getAnimatedValue(); + int numParameters = values.length; + if (mJniSetter != 0) { + switch (numParameters) { + case 1: + nCallFloatMethod(target, mJniSetter, values[0]); + break; + case 2: + nCallTwoFloatMethod(target, mJniSetter, values[0], values[1]); + break; + case 4: + nCallFourFloatMethod(target, mJniSetter, values[0], values[1], + values[2], values[3]); + break; + default: { + nCallMultipleFloatMethod(target, mJniSetter, values); + break; + } + } + } + } + + /** + * Internal function (called from ObjectAnimator) to set up the setter and getter + * prior to running the animation. No getter can be used for multiple parameters. + * + * @param target The object on which the setter exists. + */ + @Override + void setupSetterAndGetter(Object target) { + setupSetter(target.getClass()); + } + + @Override + void setupSetter(Class targetClass) { + if (mJniSetter != 0) { + return; + } + try { + mPropertyMapLock.writeLock().lock(); + HashMap<String, Long> propertyMap = sJNISetterPropertyMap.get(targetClass); + if (propertyMap != null) { + Long jniSetterLong = propertyMap.get(mPropertyName); + if (jniSetterLong != null) { + mJniSetter = jniSetterLong; + } + } + if (mJniSetter == 0) { + String methodName = getMethodName("set", mPropertyName); + calculateValue(0f); + float[] values = (float[]) getAnimatedValue(); + int numParams = values.length; + try { + mJniSetter = nGetMultipleFloatMethod(targetClass, methodName, numParams); + } catch (NoSuchMethodError e) { + // try without the 'set' prefix + mJniSetter = nGetMultipleFloatMethod(targetClass, mPropertyName, numParams); + } + if (mJniSetter != 0) { + if (propertyMap == null) { + propertyMap = new HashMap<String, Long>(); + sJNISetterPropertyMap.put(targetClass, propertyMap); + } + propertyMap.put(mPropertyName, mJniSetter); + } + } + } finally { + mPropertyMapLock.writeLock().unlock(); + } + } + } + + static class MultiIntValuesHolder extends PropertyValuesHolder { + private long mJniSetter; + private static final HashMap<Class, HashMap<String, Long>> sJNISetterPropertyMap = + new HashMap<Class, HashMap<String, Long>>(); + + public MultiIntValuesHolder(String propertyName, TypeConverter converter, + TypeEvaluator evaluator, Object... values) { + super(propertyName); + setConverter(converter); + setObjectValues(values); + setEvaluator(evaluator); + } + + public MultiIntValuesHolder(String propertyName, TypeConverter converter, + TypeEvaluator evaluator, KeyframeSet keyframeSet) { + super(propertyName); + setConverter(converter); + mKeyframeSet = keyframeSet; + setEvaluator(evaluator); + } + + /** + * Internal function to set the value on the target object, using the setter set up + * earlier on this PropertyValuesHolder object. This function is called by ObjectAnimator + * to handle turning the value calculated by ValueAnimator into a value set on the object + * according to the name of the property. + * + * @param target The target object on which the value is set + */ + @Override + void setAnimatedValue(Object target) { + int[] values = (int[]) getAnimatedValue(); + int numParameters = values.length; + if (mJniSetter != 0) { + switch (numParameters) { + case 1: + nCallIntMethod(target, mJniSetter, values[0]); + break; + case 2: + nCallTwoIntMethod(target, mJniSetter, values[0], values[1]); + break; + case 4: + nCallFourIntMethod(target, mJniSetter, values[0], values[1], + values[2], values[3]); + break; + default: { + nCallMultipleIntMethod(target, mJniSetter, values); + break; + } + } + } + } + + /** + * Internal function (called from ObjectAnimator) to set up the setter and getter + * prior to running the animation. No getter can be used for multiple parameters. + * + * @param target The object on which the setter exists. + */ + @Override + void setupSetterAndGetter(Object target) { + setupSetter(target.getClass()); + } + + @Override + void setupSetter(Class targetClass) { + if (mJniSetter != 0) { + return; + } + try { + mPropertyMapLock.writeLock().lock(); + HashMap<String, Long> propertyMap = sJNISetterPropertyMap.get(targetClass); + if (propertyMap != null) { + Long jniSetterLong = propertyMap.get(mPropertyName); + if (jniSetterLong != null) { + mJniSetter = jniSetterLong; + } + } + if (mJniSetter == 0) { + String methodName = getMethodName("set", mPropertyName); + calculateValue(0f); + int[] values = (int[]) getAnimatedValue(); + int numParams = values.length; + try { + mJniSetter = nGetMultipleIntMethod(targetClass, methodName, numParams); + } catch (NoSuchMethodError e) { + // try without the 'set' prefix + mJniSetter = nGetMultipleIntMethod(targetClass, mPropertyName, numParams); + } + if (mJniSetter != 0) { + if (propertyMap == null) { + propertyMap = new HashMap<String, Long>(); + sJNISetterPropertyMap.put(targetClass, propertyMap); + } + propertyMap.put(mPropertyName, mJniSetter); + } + } + } finally { + mPropertyMapLock.writeLock().unlock(); + } + } + } + + /* Path interpolation relies on approximating the Path as a series of line segments. + The line segments are recursively divided until there is less than 1/2 pixel error + between the lines and the curve. Each point of the line segment is converted + to a Keyframe and a linear interpolation between Keyframes creates a good approximation + of the curve. + + The fraction for each Keyframe is the length along the Path to the point, divided by + the total Path length. Two points may have the same fraction in the case of a move + command causing a disjoint Path. + + The value for each Keyframe is either the point as a PointF or one of the x or y + coordinates as an int or float. In the latter case, two Keyframes are generated for + each point that have the same fraction. */ + + /** + * Returns separate Keyframes arrays for the x and y coordinates along a Path. If + * isInt is true, the Keyframes will be IntKeyframes, otherwise they will be FloatKeyframes. + * The element at index 0 are the x coordinate Keyframes and element at index 1 are the + * y coordinate Keyframes. The returned values can be linearly interpolated and get less + * than 1/2 pixel error. + */ + static Keyframe[][] createKeyframes(Path path, boolean isInt) { + if (path == null || path.isEmpty()) { + throw new IllegalArgumentException("The path must not be null or empty"); + } + float[] pointComponents = path.approximate(0.5f); + + int numPoints = pointComponents.length / 3; + + Keyframe[][] keyframes = new Keyframe[2][]; + keyframes[0] = new Keyframe[numPoints]; + keyframes[1] = new Keyframe[numPoints]; + int componentIndex = 0; + for (int i = 0; i < numPoints; i++) { + float fraction = pointComponents[componentIndex++]; + float x = pointComponents[componentIndex++]; + float y = pointComponents[componentIndex++]; + if (isInt) { + keyframes[0][i] = Keyframe.ofInt(fraction, Math.round(x)); + keyframes[1][i] = Keyframe.ofInt(fraction, Math.round(y)); + } else { + keyframes[0][i] = Keyframe.ofFloat(fraction, x); + keyframes[1][i] = Keyframe.ofFloat(fraction, y); + } + } + return keyframes; + } + + /** + * Returns PointF Keyframes for a Path. The resulting points can be linearly interpolated + * with less than 1/2 pixel in error. + */ + private static Keyframe[] createKeyframes(Path path) { + if (path == null || path.isEmpty()) { + throw new IllegalArgumentException("The path must not be null or empty"); + } + float[] pointComponents = path.approximate(0.5f); + + int numPoints = pointComponents.length / 3; + + Keyframe[] keyframes = new Keyframe[numPoints]; + int componentIndex = 0; + for (int i = 0; i < numPoints; i++) { + float fraction = pointComponents[componentIndex++]; + float x = pointComponents[componentIndex++]; + float y = pointComponents[componentIndex++]; + keyframes[i] = Keyframe.ofObject(fraction, new PointF(x, y)); + } + return keyframes; + } + + /** + * Convert from PointF to float[] for multi-float setters along a Path. + */ + private static class PointFToFloatArray extends TypeConverter<PointF, float[]> { + private float[] mCoordinates = new float[2]; + + public PointFToFloatArray() { + super(PointF.class, float[].class); + } + + @Override + public float[] convert(PointF value) { + mCoordinates[0] = value.x; + mCoordinates[1] = value.y; + return mCoordinates; + } + }; + + /** + * Convert from PointF to int[] for multi-int setters along a Path. + */ + private static class PointFToIntArray extends TypeConverter<PointF, int[]> { + private int[] mCoordinates = new int[2]; + + public PointFToIntArray() { + super(PointF.class, int[].class); + } + + @Override + public int[] convert(PointF value) { + mCoordinates[0] = Math.round(value.x); + mCoordinates[1] = Math.round(value.y); + return mCoordinates; + } + }; + native static private long nGetIntMethod(Class targetClass, String methodName); native static private long nGetFloatMethod(Class targetClass, String methodName); + native static private long nGetMultipleIntMethod(Class targetClass, String methodName, + int numParams); + native static private long nGetMultipleFloatMethod(Class targetClass, String methodName, + int numParams); native static private void nCallIntMethod(Object target, long methodID, int arg); native static private void nCallFloatMethod(Object target, long methodID, float arg); + native static private void nCallTwoIntMethod(Object target, long methodID, int arg1, int arg2); + native static private void nCallFourIntMethod(Object target, long methodID, int arg1, int arg2, + int arg3, int arg4); + native static private void nCallMultipleIntMethod(Object target, long methodID, int[] args); + native static private void nCallTwoFloatMethod(Object target, long methodID, float arg1, + float arg2); + native static private void nCallFourFloatMethod(Object target, long methodID, float arg1, + float arg2, float arg3, float arg4); + native static private void nCallMultipleFloatMethod(Object target, long methodID, float[] args); } diff --git a/core/java/android/animation/RectEvaluator.java b/core/java/android/animation/RectEvaluator.java index 28d496b..23eb766 100644 --- a/core/java/android/animation/RectEvaluator.java +++ b/core/java/android/animation/RectEvaluator.java @@ -23,12 +23,45 @@ import android.graphics.Rect; public class RectEvaluator implements TypeEvaluator<Rect> { /** + * When null, a new Rect is returned on every evaluate call. When non-null, + * mRect will be modified and returned on every evaluate. + */ + private Rect mRect; + + /** + * Construct a RectEvaluator that returns a new Rect on every evaluate call. + * To avoid creating an object for each evaluate call, + * {@link RectEvaluator#RectEvaluator(android.graphics.Rect)} should be used + * whenever possible. + */ + public RectEvaluator() { + } + + /** + * Constructs a RectEvaluator that modifies and returns <code>reuseRect</code> + * in {@link #evaluate(float, android.graphics.Rect, android.graphics.Rect)} calls. + * The value returned from + * {@link #evaluate(float, android.graphics.Rect, android.graphics.Rect)} should + * not be cached because it will change over time as the object is reused on each + * call. + * + * @param reuseRect A Rect to be modified and returned by evaluate. + */ + public RectEvaluator(Rect reuseRect) { + mRect = reuseRect; + } + + /** * This function returns the result of linearly interpolating the start and * end Rect values, with <code>fraction</code> representing the proportion * between the start and end values. The calculation is a simple parametric * calculation on each of the separate components in the Rect objects * (left, top, right, and bottom). * + * <p>If {@link #RectEvaluator(android.graphics.Rect)} was used to construct + * this RectEvaluator, the object returned will be the <code>reuseRect</code> + * passed into the constructor.</p> + * * @param fraction The fraction from the starting to the ending values * @param startValue The start Rect * @param endValue The end Rect @@ -37,9 +70,15 @@ public class RectEvaluator implements TypeEvaluator<Rect> { */ @Override public Rect evaluate(float fraction, Rect startValue, Rect endValue) { - return new Rect(startValue.left + (int)((endValue.left - startValue.left) * fraction), - startValue.top + (int)((endValue.top - startValue.top) * fraction), - startValue.right + (int)((endValue.right - startValue.right) * fraction), - startValue.bottom + (int)((endValue.bottom - startValue.bottom) * fraction)); + int left = startValue.left + (int) ((endValue.left - startValue.left) * fraction); + int top = startValue.top + (int) ((endValue.top - startValue.top) * fraction); + int right = startValue.right + (int) ((endValue.right - startValue.right) * fraction); + int bottom = startValue.bottom + (int) ((endValue.bottom - startValue.bottom) * fraction); + if (mRect == null) { + return new Rect(left, top, right, bottom); + } else { + mRect.set(left, top, right, bottom); + return mRect; + } } } diff --git a/core/java/android/animation/RevealAnimator.java b/core/java/android/animation/RevealAnimator.java new file mode 100644 index 0000000..77a536a --- /dev/null +++ b/core/java/android/animation/RevealAnimator.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2014 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.animation; + +import android.view.View; + +import java.util.ArrayList; + +/** + * Reveals a View with an animated clipping circle. + * The clipping is implemented efficiently by talking to a private reveal API on View. + * This hidden class currently only accessed by the {@link android.view.View}. + * + * @hide + */ +public class RevealAnimator extends ValueAnimator { + private final static String LOGTAG = "RevealAnimator"; + private ValueAnimator.AnimatorListener mListener; + private ValueAnimator.AnimatorUpdateListener mUpdateListener; + private RevealCircle mReuseRevealCircle = new RevealCircle(0); + private RevealAnimator(final View clipView, final int x, final int y, + float startRadius, float endRadius, final boolean inverseClip) { + + setObjectValues(new RevealCircle(startRadius), new RevealCircle(endRadius)); + setEvaluator(new RevealCircleEvaluator(mReuseRevealCircle)); + + mUpdateListener = new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + RevealCircle circle = (RevealCircle) animation.getAnimatedValue(); + float radius = circle.getRadius(); + clipView.setRevealClip(true, inverseClip, x, y, radius); + } + }; + mListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(Animator animation) { + clipView.setRevealClip(false, false, 0, 0, 0); + } + + @Override + public void onAnimationEnd(Animator animation) { + clipView.setRevealClip(false, false, 0, 0, 0); + } + }; + addUpdateListener(mUpdateListener); + addListener(mListener); + } + + public static RevealAnimator ofRevealCircle(View clipView, int x, int y, + float startRadius, float endRadius, boolean inverseClip) { + RevealAnimator anim = new RevealAnimator(clipView, x, y, + startRadius, endRadius, inverseClip); + return anim; + } + + + /** + * {@inheritDoc} + */ + @Override + public void removeAllUpdateListeners() { + super.removeAllUpdateListeners(); + addUpdateListener(mUpdateListener); + } + + /** + * {@inheritDoc} + */ + @Override + public void removeAllListeners() { + super.removeAllListeners(); + addListener(mListener); + } + + /** + * {@inheritDoc} + */ + @Override + public ArrayList<AnimatorListener> getListeners() { + ArrayList<AnimatorListener> allListeners = + (ArrayList<AnimatorListener>) super.getListeners().clone(); + allListeners.remove(mListener); + return allListeners; + } + + private class RevealCircle { + float mRadius; + + public RevealCircle(float radius) { + mRadius = radius; + } + + public void setRadius(float radius) { + mRadius = radius; + } + + public float getRadius() { + return mRadius; + } + } + + private class RevealCircleEvaluator implements TypeEvaluator<RevealCircle> { + + private RevealCircle mRevealCircle; + + public RevealCircleEvaluator() { + } + + public RevealCircleEvaluator(RevealCircle reuseCircle) { + mRevealCircle = reuseCircle; + } + + @Override + public RevealCircle evaluate(float fraction, RevealCircle startValue, + RevealCircle endValue) { + float currentRadius = startValue.mRadius + + ((endValue.mRadius - startValue.mRadius) * fraction); + if (mRevealCircle == null) { + return new RevealCircle(currentRadius); + } else { + mRevealCircle.setRadius(currentRadius); + return mRevealCircle; + } + } + } +} diff --git a/core/java/android/animation/TypeConverter.java b/core/java/android/animation/TypeConverter.java new file mode 100644 index 0000000..03b3eb5 --- /dev/null +++ b/core/java/android/animation/TypeConverter.java @@ -0,0 +1,68 @@ +/* + * 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.animation; + +/** + * Abstract base class used convert type T to another type V. This + * is necessary when the value types of in animation are different + * from the property type. + * @see PropertyValuesHolder#setConverter(TypeConverter) + */ +public abstract class TypeConverter<T, V> { + private Class<T> mFromClass; + private Class<V> mToClass; + + public TypeConverter(Class<T> fromClass, Class<V> toClass) { + mFromClass = fromClass; + mToClass = toClass; + } + + /** + * Returns the target converted type. Used by the animation system to determine + * the proper setter function to call. + * @return The Class to convert the input to. + */ + Class<V> getTargetType() { + return mToClass; + } + + /** + * Returns the source conversion type. + */ + Class<T> getSourceType() { + return mFromClass; + } + + /** + * Converts a value from one type to another. + * @param value The Object to convert. + * @return A value of type V, converted from <code>value</code>. + */ + public abstract V convert(T value); + + /** + * Does a conversion from the target type back to the source type. The subclass + * must implement this when a TypeConverter is used in animations and current + * values will need to be read for an animation. By default, this will return null, + * indicating that back-conversion is not supported. + * @param value The Object to convert. + * @return A value of type T, converted from <code>value</code>. + */ + public T convertBack(V value) { + return null; + } +} diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java index 86da673..5338dd0 100644 --- a/core/java/android/animation/ValueAnimator.java +++ b/core/java/android/animation/ValueAnimator.java @@ -280,6 +280,24 @@ public class ValueAnimator extends Animator { } /** + * Constructs and returns a ValueAnimator that animates between color values. A single + * value implies that that value is the one being animated to. However, this is not typically + * useful in a ValueAnimator object because there is no way for the object to determine the + * starting value for the animation (unlike ObjectAnimator, which can derive that value + * from the target object and property being animated). Therefore, there should typically + * be two or more values. + * + * @param values A set of values that the animation will animate between over time. + * @return A ValueAnimator object that is set up to animate between the given values. + */ + public static ValueAnimator ofArgb(int... values) { + ValueAnimator anim = new ValueAnimator(); + anim.setIntValues(values); + anim.setEvaluator(ArgbEvaluator.getInstance()); + return anim; + } + + /** * Constructs and returns a ValueAnimator that animates between float values. A single * value implies that that value is the one being animated to. However, this is not typically * useful in a ValueAnimator object because there is no way for the object to determine the @@ -1106,28 +1124,27 @@ public class ValueAnimator extends Animator { if (!mStartedDelay) { mStartedDelay = true; mDelayStartTime = currentTime; - } else { - if (mPaused) { - if (mPauseTime < 0) { - mPauseTime = currentTime; - } - return false; - } else if (mResumed) { - mResumed = false; - if (mPauseTime > 0) { - // Offset by the duration that the animation was paused - mDelayStartTime += (currentTime - mPauseTime); - } + } + if (mPaused) { + if (mPauseTime < 0) { + mPauseTime = currentTime; } - long deltaTime = currentTime - mDelayStartTime; - if (deltaTime > mStartDelay) { - // startDelay ended - start the anim and record the - // mStartTime appropriately - mStartTime = currentTime - (deltaTime - mStartDelay); - mPlayingState = RUNNING; - return true; + return false; + } else if (mResumed) { + mResumed = false; + if (mPauseTime > 0) { + // Offset by the duration that the animation was paused + mDelayStartTime += (currentTime - mPauseTime); } } + long deltaTime = currentTime - mDelayStartTime; + if (deltaTime > mStartDelay) { + // startDelay ended - start the anim and record the + // mStartTime appropriately + mStartTime = currentTime - (deltaTime - mStartDelay); + mPlayingState = RUNNING; + return true; + } return false; } diff --git a/core/java/android/annotation/AnimRes.java b/core/java/android/annotation/AnimRes.java new file mode 100644 index 0000000..56f8acf --- /dev/null +++ b/core/java/android/annotation/AnimRes.java @@ -0,0 +1,37 @@ +/* + * 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that an integer parameter, field or method return value is expected + * to be an anim resource reference (e.g. {@link android.R.anim#fade_in}). + * + * {@hide} + */ +@Documented +@Retention(SOURCE) +@Target({METHOD, PARAMETER, FIELD}) +public @interface AnimRes { +} diff --git a/core/java/android/annotation/AnimatorRes.java b/core/java/android/annotation/AnimatorRes.java new file mode 100644 index 0000000..cd4c189 --- /dev/null +++ b/core/java/android/annotation/AnimatorRes.java @@ -0,0 +1,37 @@ +/* + * 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that an integer parameter, field or method return value is expected + * to be an animator resource reference (e.g. {@link android.R.animator#fade_in}). + * + * {@hide} + */ +@Documented +@Retention(SOURCE) +@Target({METHOD, PARAMETER, FIELD}) +public @interface AnimatorRes { +} diff --git a/core/java/android/annotation/AnyRes.java b/core/java/android/annotation/AnyRes.java new file mode 100644 index 0000000..44411a0 --- /dev/null +++ b/core/java/android/annotation/AnyRes.java @@ -0,0 +1,39 @@ +/* + * 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that an integer parameter, field or method return value is expected + * to be a resource reference of any type. If the specific type is known, use + * one of the more specific annotations instead, such as {@link StringRes} or + * {@link DrawableRes}. + * + * {@hide} + */ +@Documented +@Retention(SOURCE) +@Target({METHOD, PARAMETER, FIELD}) +public @interface AnyRes { +} diff --git a/core/java/android/annotation/ArrayRes.java b/core/java/android/annotation/ArrayRes.java new file mode 100644 index 0000000..1407af1 --- /dev/null +++ b/core/java/android/annotation/ArrayRes.java @@ -0,0 +1,37 @@ +/* + * 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that an integer parameter, field or method return value is expected + * to be an array resource reference (e.g. {@link android.R.array#phoneTypes}). + * + * {@hide} + */ +@Documented +@Retention(SOURCE) +@Target({METHOD, PARAMETER, FIELD}) +public @interface ArrayRes { +} diff --git a/core/java/android/annotation/AttrRes.java b/core/java/android/annotation/AttrRes.java new file mode 100644 index 0000000..285b80c --- /dev/null +++ b/core/java/android/annotation/AttrRes.java @@ -0,0 +1,37 @@ +/* + * 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that an integer parameter, field or method return value is expected + * to be an attribute reference (e.g. {@link android.R.attr#action}). + * + * {@hide} + */ +@Documented +@Retention(SOURCE) +@Target({METHOD, PARAMETER, FIELD}) +public @interface AttrRes { +} diff --git a/core/java/android/annotation/BoolRes.java b/core/java/android/annotation/BoolRes.java new file mode 100644 index 0000000..f50785b --- /dev/null +++ b/core/java/android/annotation/BoolRes.java @@ -0,0 +1,37 @@ +/* + * 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that an integer parameter, field or method return value is expected + * to be a boolean resource reference. + * + * {@hide} + */ +@Documented +@Retention(SOURCE) +@Target({METHOD, PARAMETER, FIELD}) +public @interface BoolRes { +} diff --git a/core/java/android/annotation/ColorRes.java b/core/java/android/annotation/ColorRes.java new file mode 100644 index 0000000..061faa0 --- /dev/null +++ b/core/java/android/annotation/ColorRes.java @@ -0,0 +1,37 @@ +/* + * 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that an integer parameter, field or method return value is expected + * to be a color resource reference (e.g. {@link android.R.color#black}). + * + * {@hide} + */ +@Documented +@Retention(SOURCE) +@Target({METHOD, PARAMETER, FIELD}) +public @interface ColorRes { +} diff --git a/core/java/android/annotation/DimenRes.java b/core/java/android/annotation/DimenRes.java new file mode 100644 index 0000000..02ae00c --- /dev/null +++ b/core/java/android/annotation/DimenRes.java @@ -0,0 +1,37 @@ +/* + * 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that an integer parameter, field or method return value is expected + * to be a dimension resource reference (e.g. {@link android.R.dimen#app_icon_size}). + * + * {@hide} + */ +@Documented +@Retention(SOURCE) +@Target({METHOD, PARAMETER, FIELD}) +public @interface DimenRes { +} diff --git a/core/java/android/annotation/DrawableRes.java b/core/java/android/annotation/DrawableRes.java new file mode 100644 index 0000000..ebefa1d --- /dev/null +++ b/core/java/android/annotation/DrawableRes.java @@ -0,0 +1,37 @@ +/* + * 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that an integer parameter, field or method return value is expected + * to be a drawable resource reference (e.g. {@link android.R.attr#alertDialogIcon}). + * + * {@hide} + */ +@Documented +@Retention(SOURCE) +@Target({METHOD, PARAMETER, FIELD}) +public @interface DrawableRes { +} diff --git a/core/java/android/annotation/FractionRes.java b/core/java/android/annotation/FractionRes.java new file mode 100644 index 0000000..fd84d3e --- /dev/null +++ b/core/java/android/annotation/FractionRes.java @@ -0,0 +1,37 @@ +/* + * 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that an integer parameter, field or method return value is expected + * to be a fraction resource reference. + * + * {@hide} + */ +@Documented +@Retention(SOURCE) +@Target({METHOD, PARAMETER, FIELD}) +public @interface FractionRes { +} diff --git a/core/java/android/annotation/IdRes.java b/core/java/android/annotation/IdRes.java new file mode 100644 index 0000000..b286965 --- /dev/null +++ b/core/java/android/annotation/IdRes.java @@ -0,0 +1,37 @@ +/* + * 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that an integer parameter, field or method return value is expected + * to be an id resource reference (e.g. {@link android.R.id#copy}). + * + * {@hide} + */ +@Documented +@Retention(SOURCE) +@Target({METHOD, PARAMETER, FIELD}) +public @interface IdRes { +} diff --git a/core/java/android/annotation/IntDef.java b/core/java/android/annotation/IntDef.java new file mode 100644 index 0000000..3cae9c5 --- /dev/null +++ b/core/java/android/annotation/IntDef.java @@ -0,0 +1,60 @@ +/* + * 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.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.RetentionPolicy.CLASS; + +/** + * Denotes that the annotated element of integer type, represents + * a logical type and that its value should be one of the explicitly + * named constants. If the {@link #flag()} attribute is set to true, + * multiple constants can be combined. + * <p> + * Example: + * <pre>{@code + * @Retention(CLASS) + * @IntDef({NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS}) + * public @interface NavigationMode {} + * public static final int NAVIGATION_MODE_STANDARD = 0; + * public static final int NAVIGATION_MODE_LIST = 1; + * public static final int NAVIGATION_MODE_TABS = 2; + * ... + * public abstract void setNavigationMode(@NavigationMode int mode); + * @NavigationMode + * public abstract int getNavigationMode(); + * }</pre> + * For a flag, set the flag attribute: + * <pre>{@code + * @IntDef( + * flag = true + * value = {NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS}) + * }</pre> + * + * @hide + */ +@Retention(CLASS) +@Target({ANNOTATION_TYPE}) +public @interface IntDef { + /** Defines the allowed constants for this element */ + long[] value() default {}; + + /** Defines whether the constants can be used as a flag, or just as an enum (the default) */ + boolean flag() default false; +} diff --git a/core/java/android/annotation/IntegerRes.java b/core/java/android/annotation/IntegerRes.java new file mode 100644 index 0000000..5313f4a --- /dev/null +++ b/core/java/android/annotation/IntegerRes.java @@ -0,0 +1,37 @@ +/* + * 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that an integer parameter, field or method return value is expected + * to be an integer resource reference (e.g. {@link android.R.integer#config_shortAnimTime}). + * + * {@hide} + */ +@Documented +@Retention(SOURCE) +@Target({METHOD, PARAMETER, FIELD}) +public @interface IntegerRes { +} diff --git a/core/java/android/annotation/InterpolatorRes.java b/core/java/android/annotation/InterpolatorRes.java new file mode 100644 index 0000000..8877a5f --- /dev/null +++ b/core/java/android/annotation/InterpolatorRes.java @@ -0,0 +1,37 @@ +/* + * 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that an integer parameter, field or method return value is expected + * to be an interpolator resource reference (e.g. {@link android.R.interpolator#cycle}). + * + * {@hide} + */ +@Documented +@Retention(SOURCE) +@Target({METHOD, PARAMETER, FIELD}) +public @interface InterpolatorRes { +} diff --git a/core/java/android/annotation/LayoutRes.java b/core/java/android/annotation/LayoutRes.java new file mode 100644 index 0000000..15ba86f --- /dev/null +++ b/core/java/android/annotation/LayoutRes.java @@ -0,0 +1,37 @@ +/* + * 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that an integer parameter, field or method return value is expected + * to be a layout resource reference (e.g. {@link android.R.layout#list_content}). + * + * {@hide} + */ +@Documented +@Retention(SOURCE) +@Target({METHOD, PARAMETER, FIELD}) +public @interface LayoutRes { +} diff --git a/core/java/android/annotation/MenuRes.java b/core/java/android/annotation/MenuRes.java new file mode 100644 index 0000000..b6dcc46 --- /dev/null +++ b/core/java/android/annotation/MenuRes.java @@ -0,0 +1,37 @@ +/* + * 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that an integer parameter, field or method return value is expected + * to be a menu resource reference. + * + * {@hide} + */ +@Documented +@Retention(SOURCE) +@Target({METHOD, PARAMETER, FIELD}) +public @interface MenuRes { +} diff --git a/core/java/android/annotation/NonNull.java b/core/java/android/annotation/NonNull.java new file mode 100644 index 0000000..3ca9eea --- /dev/null +++ b/core/java/android/annotation/NonNull.java @@ -0,0 +1,36 @@ +/* + * 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.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that a parameter, field or method return value can never be null. + * <p> + * This is a marker annotation and it has no specific attributes. + * + * @hide + */ +@Retention(SOURCE) +@Target({METHOD, PARAMETER, FIELD}) +public @interface NonNull { +} diff --git a/core/java/android/annotation/Nullable.java b/core/java/android/annotation/Nullable.java new file mode 100644 index 0000000..43f42fa --- /dev/null +++ b/core/java/android/annotation/Nullable.java @@ -0,0 +1,43 @@ +/* + * 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.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that a parameter, field or method return value can be null. + * <p> + * When decorating a method call parameter, this denotes that the parameter can + * legitimately be null and the method will gracefully deal with it. Typically + * used on optional parameters. + * <p> + * When decorating a method, this denotes the method might legitimately return + * null. + * <p> + * This is a marker annotation and it has no specific attributes. + * + * @hide + */ +@Retention(SOURCE) +@Target({METHOD, PARAMETER, FIELD}) +public @interface Nullable { +} diff --git a/core/java/android/annotation/PluralsRes.java b/core/java/android/annotation/PluralsRes.java new file mode 100644 index 0000000..31ac729 --- /dev/null +++ b/core/java/android/annotation/PluralsRes.java @@ -0,0 +1,37 @@ +/* + * 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that an integer parameter, field or method return value is expected + * to be a plurals resource reference. + * + * {@hide} + */ +@Documented +@Retention(SOURCE) +@Target({METHOD, PARAMETER, FIELD}) +public @interface PluralsRes { +} diff --git a/core/java/android/annotation/RawRes.java b/core/java/android/annotation/RawRes.java new file mode 100644 index 0000000..39970b3 --- /dev/null +++ b/core/java/android/annotation/RawRes.java @@ -0,0 +1,37 @@ +/* + * 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that an integer parameter, field or method return value is expected + * to be a raw resource reference. + * + * {@hide} + */ +@Documented +@Retention(SOURCE) +@Target({METHOD, PARAMETER, FIELD}) +public @interface RawRes { +} diff --git a/core/java/android/annotation/StringDef.java b/core/java/android/annotation/StringDef.java new file mode 100644 index 0000000..5f7f380 --- /dev/null +++ b/core/java/android/annotation/StringDef.java @@ -0,0 +1,51 @@ +/* + * 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.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.RetentionPolicy.CLASS; + +/** + * Denotes that the annotated String element, represents a logical + * type and that its value should be one of the explicitly named constants. + * <p> + * Example: + * <pre>{@code + * @Retention(SOURCE) + * @StringDef({ + * POWER_SERVICE, + * WINDOW_SERVICE, + * LAYOUT_INFLATER_SERVICE + * }) + * public @interface ServiceName {} + * public static final String POWER_SERVICE = "power"; + * public static final String WINDOW_SERVICE = "window"; + * public static final String LAYOUT_INFLATER_SERVICE = "layout_inflater"; + * ... + * public abstract Object getSystemService(@ServiceName String name); + * }</pre> + * + * @hide + */ +@Retention(CLASS) +@Target({ANNOTATION_TYPE}) +public @interface StringDef { + /** Defines the allowed constants for this element */ + String[] value() default {}; +} diff --git a/core/java/android/annotation/StringRes.java b/core/java/android/annotation/StringRes.java new file mode 100644 index 0000000..190b68a --- /dev/null +++ b/core/java/android/annotation/StringRes.java @@ -0,0 +1,37 @@ +/* + * 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that an integer parameter, field or method return value is expected + * to be a String resource reference (e.g. {@link android.R.string#ok}). + * + * {@hide} + */ +@Documented +@Retention(SOURCE) +@Target({METHOD, PARAMETER, FIELD}) +public @interface StringRes { +} diff --git a/core/java/android/annotation/StyleRes.java b/core/java/android/annotation/StyleRes.java new file mode 100644 index 0000000..4453b8d --- /dev/null +++ b/core/java/android/annotation/StyleRes.java @@ -0,0 +1,37 @@ +/* + * 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that a integer parameter, field or method return value is expected + * to be a style resource reference (e.g. {@link android.R.style#TextAppearance}). + * + * {@hide} + */ +@Documented +@Retention(SOURCE) +@Target({METHOD, PARAMETER, FIELD}) +public @interface StyleRes { +} diff --git a/core/java/android/annotation/StyleableRes.java b/core/java/android/annotation/StyleableRes.java new file mode 100644 index 0000000..3c1895e --- /dev/null +++ b/core/java/android/annotation/StyleableRes.java @@ -0,0 +1,37 @@ +/* + * 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that a integer parameter, field or method return value is expected + * to be a styleable resource reference (e.g. {@link android.R.styleable#TextView_text}). + * + * {@hide} + */ +@Documented +@Retention(SOURCE) +@Target({METHOD, PARAMETER, FIELD}) +public @interface StyleableRes { +} diff --git a/core/java/android/annotation/XmlRes.java b/core/java/android/annotation/XmlRes.java new file mode 100644 index 0000000..5fb8a4a --- /dev/null +++ b/core/java/android/annotation/XmlRes.java @@ -0,0 +1,37 @@ +/* + * 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that an integer parameter, field or method return value is expected + * to be an XML resource reference. + * + * {@hide} + */ +@Documented +@Retention(SOURCE) +@Target({METHOD, PARAMETER, FIELD}) +public @interface XmlRes { +} diff --git a/core/java/android/app/ActionBar.java b/core/java/android/app/ActionBar.java index c4ddf1f..9818c33 100644 --- a/core/java/android/app/ActionBar.java +++ b/core/java/android/app/ActionBar.java @@ -16,10 +16,15 @@ package android.app; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; +import android.content.res.Configuration; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.util.AttributeSet; +import android.view.ActionMode; import android.view.Gravity; import android.view.View; import android.view.ViewDebug; @@ -27,28 +32,57 @@ import android.view.ViewGroup; import android.view.ViewGroup.MarginLayoutParams; import android.view.Window; import android.widget.SpinnerAdapter; +import android.widget.Toolbar; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Map; /** - * A window feature at the top of the activity that may display the activity title, navigation - * modes, and other interactive items. + * A primary toolbar within the activity that may display the activity title, application-level + * navigation affordances, and other interactive items. + * * <p>Beginning with Android 3.0 (API level 11), the action bar appears at the top of an * activity's window when the activity uses the system's {@link * android.R.style#Theme_Holo Holo} theme (or one of its descendant themes), which is the default. * You may otherwise add the action bar by calling {@link * android.view.Window#requestFeature requestFeature(FEATURE_ACTION_BAR)} or by declaring it in a * custom theme with the {@link android.R.styleable#Theme_windowActionBar windowActionBar} property. - * <p>By default, the action bar shows the application icon on + * </p> + * + * <p>Beginning with Android L (API level 21), the action bar may be represented by any + * Toolbar widget within the application layout. The application may signal to the Activity + * which Toolbar should be treated as the Activity's action bar. Activities that use this + * feature should use one of the supplied <code>.NoActionBar</code> themes, set the + * {@link android.R.styleable#Theme_windowActionBar windowActionBar} attribute to <code>false</code> + * or otherwise not request the window feature.</p> + * + * <p>By adjusting the window features requested by the theme and the layouts used for + * an Activity's content view, an app can use the standard system action bar on older platform + * releases and the newer inline toolbars on newer platform releases. The <code>ActionBar</code> + * object obtained from the Activity can be used to control either configuration transparently.</p> + * + * <p>When using the Holo themes the action bar shows the application icon on * the left, followed by the activity title. If your activity has an options menu, you can make * select items accessible directly from the action bar as "action items". You can also * modify various characteristics of the action bar or remove it completely.</p> + * + * <p>When using the Quantum themes (default in API 21 or newer) the navigation button + * (formerly "Home") takes over the space previously occupied by the application icon. + * Apps wishing to express a stronger branding should use their brand colors heavily + * in the action bar and other application chrome or use a {@link #setLogo(int) logo} + * in place of their standard title text.</p> + * * <p>From your activity, you can retrieve an instance of {@link ActionBar} by calling {@link * android.app.Activity#getActionBar getActionBar()}.</p> + * * <p>In some cases, the action bar may be overlayed by another bar that enables contextual actions, * using an {@link android.view.ActionMode}. For example, when the user selects one or more items in * your activity, you can enable an action mode that offers actions specific to the selected * items, with a UI that temporarily replaces the action bar. Although the UI may occupy the * same space, the {@link android.view.ActionMode} APIs are distinct and independent from those for - * {@link ActionBar}. + * {@link ActionBar}.</p> + * * <div class="special reference"> * <h3>Developer Guides</h3> * <p>For information about how to use the action bar, including how to add action items, navigation @@ -57,11 +91,21 @@ import android.widget.SpinnerAdapter; * </div> */ public abstract class ActionBar { + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS}) + public @interface NavigationMode {} + /** * Standard navigation mode. Consists of either a logo or icon * and title text with an optional subtitle. Clicking any of these elements * will dispatch onOptionsItemSelected to the host Activity with * a MenuItem with item ID android.R.id.home. + * + * @deprecated Action bar navigation modes are deprecated and not supported by inline + * toolbar action bars. Consider using other + * <a href="http://developer.android.com/design/patterns/navigation.html">common + * navigation patterns</a> instead. */ public static final int NAVIGATION_MODE_STANDARD = 0; @@ -69,15 +113,38 @@ public abstract class ActionBar { * List navigation mode. Instead of static title text this mode * presents a list menu for navigation within the activity. * e.g. this might be presented to the user as a dropdown list. + * + * @deprecated Action bar navigation modes are deprecated and not supported by inline + * toolbar action bars. Consider using other + * <a href="http://developer.android.com/design/patterns/navigation.html">common + * navigation patterns</a> instead. */ public static final int NAVIGATION_MODE_LIST = 1; /** * Tab navigation mode. Instead of static title text this mode * presents a series of tabs for navigation within the activity. + * + * @deprecated Action bar navigation modes are deprecated and not supported by inline + * toolbar action bars. Consider using other + * <a href="http://developer.android.com/design/patterns/navigation.html">common + * navigation patterns</a> instead. */ public static final int NAVIGATION_MODE_TABS = 2; + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, + value = { + DISPLAY_USE_LOGO, + DISPLAY_SHOW_HOME, + DISPLAY_HOME_AS_UP, + DISPLAY_SHOW_TITLE, + DISPLAY_SHOW_CUSTOM, + DISPLAY_TITLE_MULTIPLE_LINES + }) + public @interface DisplayOptions {} + /** * Use logo instead of icon if available. This flag will cause appropriate * navigation modes to use a wider logo in place of the standard icon. @@ -264,6 +331,11 @@ public abstract class ActionBar { * within the dropdown navigation menu. * @param callback An OnNavigationListener that will receive events when the user * selects a navigation item. + * + * @deprecated Action bar navigation modes are deprecated and not supported by inline + * toolbar action bars. Consider using other + * <a href="http://developer.android.com/design/patterns/navigation.html">common + * navigation patterns</a> instead. */ public abstract void setListNavigationCallbacks(SpinnerAdapter adapter, OnNavigationListener callback); @@ -272,6 +344,11 @@ public abstract class ActionBar { * Set the selected navigation item in list or tabbed navigation modes. * * @param position Position of the item to select. + * + * @deprecated Action bar navigation modes are deprecated and not supported by inline + * toolbar action bars. Consider using other + * <a href="http://developer.android.com/design/patterns/navigation.html">common + * navigation patterns</a> instead. */ public abstract void setSelectedNavigationItem(int position); @@ -279,6 +356,11 @@ public abstract class ActionBar { * Get the position of the selected navigation item in list or tabbed navigation modes. * * @return Position of the selected item. + * + * @deprecated Action bar navigation modes are deprecated and not supported by inline + * toolbar action bars. Consider using other + * <a href="http://developer.android.com/design/patterns/navigation.html">common + * navigation patterns</a> instead. */ public abstract int getSelectedNavigationIndex(); @@ -286,6 +368,11 @@ public abstract class ActionBar { * Get the number of navigation items present in the current navigation mode. * * @return Number of navigation items. + * + * @deprecated Action bar navigation modes are deprecated and not supported by inline + * toolbar action bars. Consider using other + * <a href="http://developer.android.com/design/patterns/navigation.html">common + * navigation patterns</a> instead. */ public abstract int getNavigationItemCount(); @@ -341,7 +428,7 @@ public abstract class ActionBar { * @param options A combination of the bits defined by the DISPLAY_ constants * defined in ActionBar. */ - public abstract void setDisplayOptions(int options); + public abstract void setDisplayOptions(@DisplayOptions int options); /** * Set selected display options. Only the options specified by mask will be changed. @@ -356,7 +443,7 @@ public abstract class ActionBar { * defined in ActionBar. * @param mask A bit mask declaring which display options should be changed. */ - public abstract void setDisplayOptions(int options, int mask); + public abstract void setDisplayOptions(@DisplayOptions int options, @DisplayOptions int mask); /** * Set whether to display the activity logo rather than the activity icon. @@ -431,7 +518,7 @@ public abstract class ActionBar { * @see #setStackedBackgroundDrawable(Drawable) * @see #setSplitBackgroundDrawable(Drawable) */ - public abstract void setBackgroundDrawable(Drawable d); + public abstract void setBackgroundDrawable(@Nullable Drawable d); /** * Set the ActionBar's stacked background. This will appear @@ -483,7 +570,13 @@ public abstract class ActionBar { * </ul> * * @return The current navigation mode. + * + * @deprecated Action bar navigation modes are deprecated and not supported by inline + * toolbar action bars. Consider using other + * <a href="http://developer.android.com/design/patterns/navigation.html">common + * navigation patterns</a> instead. */ + @NavigationMode public abstract int getNavigationMode(); /** @@ -493,8 +586,13 @@ public abstract class ActionBar { * @see #NAVIGATION_MODE_STANDARD * @see #NAVIGATION_MODE_LIST * @see #NAVIGATION_MODE_TABS + * + * @deprecated Action bar navigation modes are deprecated and not supported by inline + * toolbar action bars. Consider using other + * <a href="http://developer.android.com/design/patterns/navigation.html">common + * navigation patterns</a> instead. */ - public abstract void setNavigationMode(int mode); + public abstract void setNavigationMode(@NavigationMode int mode); /** * @return The current set of display options. @@ -514,6 +612,11 @@ public abstract class ActionBar { * @return A new Tab * * @see #addTab(Tab) + * + * @deprecated Action bar navigation modes are deprecated and not supported by inline + * toolbar action bars. Consider using other + * <a href="http://developer.android.com/design/patterns/navigation.html">common + * navigation patterns</a> instead. */ public abstract Tab newTab(); @@ -522,6 +625,11 @@ public abstract class ActionBar { * If this is the first tab to be added it will become the selected tab. * * @param tab Tab to add + * + * @deprecated Action bar navigation modes are deprecated and not supported by inline + * toolbar action bars. Consider using other + * <a href="http://developer.android.com/design/patterns/navigation.html">common + * navigation patterns</a> instead. */ public abstract void addTab(Tab tab); @@ -530,6 +638,11 @@ public abstract class ActionBar { * * @param tab Tab to add * @param setSelected True if the added tab should become the selected tab. + * + * @deprecated Action bar navigation modes are deprecated and not supported by inline + * toolbar action bars. Consider using other + * <a href="http://developer.android.com/design/patterns/navigation.html">common + * navigation patterns</a> instead. */ public abstract void addTab(Tab tab, boolean setSelected); @@ -540,6 +653,11 @@ public abstract class ActionBar { * * @param tab The tab to add * @param position The new position of the tab + * + * @deprecated Action bar navigation modes are deprecated and not supported by inline + * toolbar action bars. Consider using other + * <a href="http://developer.android.com/design/patterns/navigation.html">common + * navigation patterns</a> instead. */ public abstract void addTab(Tab tab, int position); @@ -550,6 +668,11 @@ public abstract class ActionBar { * @param tab The tab to add * @param position The new position of the tab * @param setSelected True if the added tab should become the selected tab. + * + * @deprecated Action bar navigation modes are deprecated and not supported by inline + * toolbar action bars. Consider using other + * <a href="http://developer.android.com/design/patterns/navigation.html">common + * navigation patterns</a> instead. */ public abstract void addTab(Tab tab, int position, boolean setSelected); @@ -558,6 +681,11 @@ public abstract class ActionBar { * and another tab will be selected if present. * * @param tab The tab to remove + * + * @deprecated Action bar navigation modes are deprecated and not supported by inline + * toolbar action bars. Consider using other + * <a href="http://developer.android.com/design/patterns/navigation.html">common + * navigation patterns</a> instead. */ public abstract void removeTab(Tab tab); @@ -566,11 +694,21 @@ public abstract class ActionBar { * and another tab will be selected if present. * * @param position Position of the tab to remove + * + * @deprecated Action bar navigation modes are deprecated and not supported by inline + * toolbar action bars. Consider using other + * <a href="http://developer.android.com/design/patterns/navigation.html">common + * navigation patterns</a> instead. */ public abstract void removeTabAt(int position); /** * Remove all tabs from the action bar and deselect the current tab. + * + * @deprecated Action bar navigation modes are deprecated and not supported by inline + * toolbar action bars. Consider using other + * <a href="http://developer.android.com/design/patterns/navigation.html">common + * navigation patterns</a> instead. */ public abstract void removeAllTabs(); @@ -580,6 +718,11 @@ public abstract class ActionBar { * <p>Note: If you want to select by index, use {@link #setSelectedNavigationItem(int)}.</p> * * @param tab Tab to select + * + * @deprecated Action bar navigation modes are deprecated and not supported by inline + * toolbar action bars. Consider using other + * <a href="http://developer.android.com/design/patterns/navigation.html">common + * navigation patterns</a> instead. */ public abstract void selectTab(Tab tab); @@ -588,6 +731,11 @@ public abstract class ActionBar { * one tab present. * * @return The currently selected tab or null + * + * @deprecated Action bar navigation modes are deprecated and not supported by inline + * toolbar action bars. Consider using other + * <a href="http://developer.android.com/design/patterns/navigation.html">common + * navigation patterns</a> instead. */ public abstract Tab getSelectedTab(); @@ -596,12 +744,22 @@ public abstract class ActionBar { * * @param index Index value in the range 0-get * @return + * + * @deprecated Action bar navigation modes are deprecated and not supported by inline + * toolbar action bars. Consider using other + * <a href="http://developer.android.com/design/patterns/navigation.html">common + * navigation patterns</a> instead. */ public abstract Tab getTabAt(int index); /** * Returns the number of tabs currently registered with the action bar. * @return Tab count + * + * @deprecated Action bar navigation modes are deprecated and not supported by inline + * toolbar action bars. Consider using other + * <a href="http://developer.android.com/design/patterns/navigation.html">common + * navigation patterns</a> instead. */ public abstract int getTabCount(); @@ -774,8 +932,38 @@ public abstract class ActionBar { */ public void setHomeActionContentDescription(int resId) { } + /** @hide */ + public void setDefaultDisplayHomeAsUpEnabled(boolean enabled) { + } + + /** @hide */ + public void setShowHideAnimationEnabled(boolean enabled) { + } + + /** @hide */ + public void onConfigurationChanged(Configuration config) { + } + + /** @hide */ + public void dispatchMenuVisibilityChanged(boolean visible) { + } + + /** @hide */ + public void captureSharedElements(Map<String, View> sharedElements) { + } + + /** @hide */ + public ActionMode startActionMode(ActionMode.Callback callback) { + return null; + } + /** * Listener interface for ActionBar navigation events. + * + * @deprecated Action bar navigation modes are deprecated and not supported by inline + * toolbar action bars. Consider using other + * <a href="http://developer.android.com/design/patterns/navigation.html">common + * navigation patterns</a> instead. */ public interface OnNavigationListener { /** @@ -808,6 +996,11 @@ public abstract class ActionBar { * A tab in the action bar. * * <p>Tabs manage the hiding and showing of {@link Fragment}s. + * + * @deprecated Action bar navigation modes are deprecated and not supported by inline + * toolbar action bars. Consider using other + * <a href="http://developer.android.com/design/patterns/navigation.html">common + * navigation patterns</a> instead. */ public static abstract class Tab { /** @@ -959,6 +1152,11 @@ public abstract class ActionBar { /** * Callback interface invoked when a tab is focused, unfocused, added, or removed. + * + * @deprecated Action bar navigation modes are deprecated and not supported by inline + * toolbar action bars. Consider using other + * <a href="http://developer.android.com/design/patterns/navigation.html">common + * navigation patterns</a> instead. */ public interface TabListener { /** @@ -1000,31 +1198,31 @@ public abstract class ActionBar { * * @attr ref android.R.styleable#ActionBar_LayoutParams_layout_gravity */ - public static class LayoutParams extends MarginLayoutParams { + public static class LayoutParams extends ViewGroup.MarginLayoutParams { /** * Gravity for the view associated with these LayoutParams. * * @see android.view.Gravity */ @ViewDebug.ExportedProperty(category = "layout", mapping = { - @ViewDebug.IntToString(from = -1, to = "NONE"), - @ViewDebug.IntToString(from = Gravity.NO_GRAVITY, to = "NONE"), - @ViewDebug.IntToString(from = Gravity.TOP, to = "TOP"), - @ViewDebug.IntToString(from = Gravity.BOTTOM, to = "BOTTOM"), - @ViewDebug.IntToString(from = Gravity.LEFT, to = "LEFT"), - @ViewDebug.IntToString(from = Gravity.RIGHT, to = "RIGHT"), - @ViewDebug.IntToString(from = Gravity.START, to = "START"), - @ViewDebug.IntToString(from = Gravity.END, to = "END"), - @ViewDebug.IntToString(from = Gravity.CENTER_VERTICAL, to = "CENTER_VERTICAL"), - @ViewDebug.IntToString(from = Gravity.FILL_VERTICAL, to = "FILL_VERTICAL"), - @ViewDebug.IntToString(from = Gravity.CENTER_HORIZONTAL, to = "CENTER_HORIZONTAL"), - @ViewDebug.IntToString(from = Gravity.FILL_HORIZONTAL, to = "FILL_HORIZONTAL"), - @ViewDebug.IntToString(from = Gravity.CENTER, to = "CENTER"), - @ViewDebug.IntToString(from = Gravity.FILL, to = "FILL") + @ViewDebug.IntToString(from = -1, to = "NONE"), + @ViewDebug.IntToString(from = Gravity.NO_GRAVITY, to = "NONE"), + @ViewDebug.IntToString(from = Gravity.TOP, to = "TOP"), + @ViewDebug.IntToString(from = Gravity.BOTTOM, to = "BOTTOM"), + @ViewDebug.IntToString(from = Gravity.LEFT, to = "LEFT"), + @ViewDebug.IntToString(from = Gravity.RIGHT, to = "RIGHT"), + @ViewDebug.IntToString(from = Gravity.START, to = "START"), + @ViewDebug.IntToString(from = Gravity.END, to = "END"), + @ViewDebug.IntToString(from = Gravity.CENTER_VERTICAL, to = "CENTER_VERTICAL"), + @ViewDebug.IntToString(from = Gravity.FILL_VERTICAL, to = "FILL_VERTICAL"), + @ViewDebug.IntToString(from = Gravity.CENTER_HORIZONTAL, to = "CENTER_HORIZONTAL"), + @ViewDebug.IntToString(from = Gravity.FILL_HORIZONTAL, to = "FILL_HORIZONTAL"), + @ViewDebug.IntToString(from = Gravity.CENTER, to = "CENTER"), + @ViewDebug.IntToString(from = Gravity.FILL, to = "FILL") }) public int gravity = Gravity.NO_GRAVITY; - public LayoutParams(Context c, AttributeSet attrs) { + public LayoutParams(@NonNull Context c, AttributeSet attrs) { super(c, attrs); TypedArray a = c.obtainStyledAttributes(attrs, @@ -1037,11 +1235,11 @@ public abstract class ActionBar { public LayoutParams(int width, int height) { super(width, height); - this.gravity = Gravity.CENTER_VERTICAL | Gravity.START; } public LayoutParams(int width, int height, int gravity) { super(width, height); + this.gravity = gravity; } @@ -1051,12 +1249,14 @@ public abstract class ActionBar { public LayoutParams(LayoutParams source) { super(source); - - this.gravity = source.gravity; } public LayoutParams(ViewGroup.LayoutParams source) { super(source); } + + public LayoutParams(MarginLayoutParams source) { + super(source); + } } } diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 63c9fec..b18eb98 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -16,11 +16,19 @@ package android.app; +import android.annotation.NonNull; +import android.transition.Scene; +import android.transition.Transition; +import android.transition.TransitionManager; import android.util.ArrayMap; import android.util.SuperNotCalledException; -import com.android.internal.app.ActionBarImpl; +import android.widget.Toolbar; +import com.android.internal.app.WindowDecorActionBar; +import com.android.internal.app.ToolbarActionBar; import com.android.internal.policy.PolicyManager; +import android.annotation.IntDef; +import android.annotation.Nullable; import android.content.ComponentCallbacks2; import android.content.ComponentName; import android.content.ContentResolver; @@ -84,6 +92,8 @@ import android.widget.AdapterView; import java.io.FileDescriptor; import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.HashMap; @@ -714,7 +724,7 @@ public class Activity extends ContextThemeWrapper /*package*/ boolean mWindowAdded = false; /*package*/ boolean mVisibleFromServer = false; /*package*/ boolean mVisibleFromClient = true; - /*package*/ ActionBarImpl mActionBar = null; + /*package*/ ActionBar mActionBar = null; private boolean mEnableDefaultActionBarUp; private CharSequence mTitle; @@ -852,6 +862,7 @@ public class Activity extends ContextThemeWrapper * @see #getWindow * @see android.view.Window#getCurrentFocus */ + @Nullable public View getCurrentFocus() { return mWindow != null ? mWindow.getCurrentFocus() : null; } @@ -882,7 +893,7 @@ public class Activity extends ContextThemeWrapper * @see #onRestoreInstanceState * @see #onPostCreate */ - protected void onCreate(Bundle savedInstanceState) { + protected void onCreate(@Nullable Bundle savedInstanceState) { if (DEBUG_LIFECYCLE) Slog.v(TAG, "onCreate " + this + ": " + savedInstanceState); if (mLastNonConfigurationInstances != null) { mAllLoaderManagers = mLastNonConfigurationInstances.loaders; @@ -1010,7 +1021,7 @@ public class Activity extends ContextThemeWrapper * recently supplied in {@link #onSaveInstanceState}. <b><i>Note: Otherwise it is null.</i></b> * @see #onCreate */ - protected void onPostCreate(Bundle savedInstanceState) { + protected void onPostCreate(@Nullable Bundle savedInstanceState) { if (!isChild()) { mTitleReady = true; onTitleChanged(getTitle(), getTitleColor()); @@ -1021,7 +1032,7 @@ public class Activity extends ContextThemeWrapper /** * Called after {@link #onCreate} — or after {@link #onRestart} when * the activity had been stopped, but is now again being displayed to the - * user. It will be followed by {@link #onResume}. + * user. It will be followed by {@link #onResume}. * * <p><em>Derived classes must call through to the super class's * implementation of this method. If they do not, an exception will be @@ -1347,6 +1358,7 @@ public class Activity extends ContextThemeWrapper * @see #onSaveInstanceState * @see #onPause */ + @Nullable public CharSequence onCreateDescription() { return null; } @@ -1386,6 +1398,9 @@ public class Activity extends ContextThemeWrapper protected void onStop() { if (DEBUG_LIFECYCLE) Slog.v(TAG, "onStop " + this); if (mActionBar != null) mActionBar.setShowHideAnimationEnabled(false); + if (mWindow != null) { + mWindow.restoreViewVisibilityAfterTransitionToCallee(); + } getApplication().dispatchActivityStopped(this); mTranslucentCallback = null; mCalled = true; @@ -1551,6 +1566,7 @@ public class Activity extends ContextThemeWrapper * {@link Fragment#setRetainInstance(boolean)} instead; this is also * available on older platforms through the Android compatibility package. */ + @Nullable @Deprecated public Object getLastNonConfigurationInstance() { return mLastNonConfigurationInstances != null @@ -1630,6 +1646,7 @@ public class Activity extends ContextThemeWrapper * @return Returns the object previously returned by * {@link #onRetainNonConfigurationChildInstances()} */ + @Nullable HashMap<String, Object> getLastNonConfigurationChildInstances() { return mLastNonConfigurationInstances != null ? mLastNonConfigurationInstances.children : null; @@ -1642,6 +1659,7 @@ public class Activity extends ContextThemeWrapper * set of child activities, such as ActivityGroup. The same guarantees and restrictions apply * as for {@link #onRetainNonConfigurationInstance()}. The default implementation returns null. */ + @Nullable HashMap<String,Object> onRetainNonConfigurationChildInstances() { return null; } @@ -1889,16 +1907,41 @@ public class Activity extends ContextThemeWrapper * * @return The Activity's ActionBar, or null if it does not have one. */ + @Nullable public ActionBar getActionBar() { - initActionBar(); + initWindowDecorActionBar(); return mActionBar; } + + /** + * Set a {@link android.widget.Toolbar Toolbar} to act as the {@link ActionBar} for this + * Activity window. + * + * <p>When set to a non-null value the {@link #getActionBar()} method will return + * an {@link ActionBar} object that can be used to control the given toolbar as if it were + * a traditional window decor action bar. The toolbar's menu will be populated with the + * Activity's options menu and the navigation button will be wired through the standard + * {@link android.R.id#home home} menu select action.</p> + * + * <p>In order to use a Toolbar within the Activity's window content the application + * must not request the window feature {@link Window#FEATURE_ACTION_BAR FEATURE_ACTION_BAR}.</p> + * + * @param actionBar Toolbar to set as the Activity's action bar + */ + public void setActionBar(@Nullable Toolbar actionBar) { + if (getActionBar() instanceof WindowDecorActionBar) { + throw new IllegalStateException("This Activity already has an action bar supplied " + + "by the window decor. Do not request Window.FEATURE_ACTION_BAR and set " + + "android:windowActionBar to false in your theme to use a Toolbar instead."); + } + mActionBar = new ToolbarActionBar(actionBar); + } /** * Creates a new ActionBar, locates the inflated ActionBarView, * initializes the ActionBar with the view, and sets mActionBar. */ - private void initActionBar() { + private void initWindowDecorActionBar() { Window window = getWindow(); // Initializing the window decor can change window feature flags. @@ -1909,7 +1952,7 @@ public class Activity extends ContextThemeWrapper return; } - mActionBar = new ActionBarImpl(this); + mActionBar = new WindowDecorActionBar(this); mActionBar.setDefaultDisplayHomeAsUpEnabled(mEnableDefaultActionBarUp); mWindow.setDefaultIcon(mActivityInfo.getIconResource()); @@ -1927,7 +1970,7 @@ public class Activity extends ContextThemeWrapper */ public void setContentView(int layoutResID) { getWindow().setContentView(layoutResID); - initActionBar(); + initWindowDecorActionBar(); } /** @@ -1947,7 +1990,7 @@ public class Activity extends ContextThemeWrapper */ public void setContentView(View view) { getWindow().setContentView(view); - initActionBar(); + initWindowDecorActionBar(); } /** @@ -1963,7 +2006,7 @@ public class Activity extends ContextThemeWrapper */ public void setContentView(View view, ViewGroup.LayoutParams params) { getWindow().setContentView(view, params); - initActionBar(); + initWindowDecorActionBar(); } /** @@ -1975,7 +2018,42 @@ public class Activity extends ContextThemeWrapper */ public void addContentView(View view, ViewGroup.LayoutParams params) { getWindow().addContentView(view, params); - initActionBar(); + initWindowDecorActionBar(); + } + + /** + * Retrieve the {@link TransitionManager} responsible for default transitions in this window. + * Requires {@link Window#FEATURE_CONTENT_TRANSITIONS}. + * + * <p>This method will return non-null after content has been initialized (e.g. by using + * {@link #setContentView}) if {@link Window#FEATURE_CONTENT_TRANSITIONS} has been granted.</p> + * + * @return This window's content TransitionManager or null if none is set. + */ + public TransitionManager getContentTransitionManager() { + return getWindow().getTransitionManager(); + } + + /** + * Set the {@link TransitionManager} to use for default transitions in this window. + * Requires {@link Window#FEATURE_CONTENT_TRANSITIONS}. + * + * @param tm The TransitionManager to use for scene changes. + */ + public void setContentTransitionManager(TransitionManager tm) { + getWindow().setTransitionManager(tm); + } + + /** + * Retrieve the {@link Scene} representing this window's current content. + * Requires {@link Window#FEATURE_CONTENT_TRANSITIONS}. + * + * <p>This method will return null if the current content is not represented by a Scene.</p> + * + * @return Current Scene being shown or null + */ + public Scene getContentScene() { + return getWindow().getContentScene(); } /** @@ -1985,7 +2063,17 @@ public class Activity extends ContextThemeWrapper public void setFinishOnTouchOutside(boolean finish) { mWindow.setCloseOnTouchOutside(finish); } - + + /** @hide */ + @IntDef({ + DEFAULT_KEYS_DISABLE, + DEFAULT_KEYS_DIALER, + DEFAULT_KEYS_SHORTCUT, + DEFAULT_KEYS_SEARCH_LOCAL, + DEFAULT_KEYS_SEARCH_GLOBAL}) + @Retention(RetentionPolicy.SOURCE) + @interface DefaultKeyMode {} + /** * Use with {@link #setDefaultKeyMode} to turn off default handling of * keys. @@ -2055,7 +2143,7 @@ public class Activity extends ContextThemeWrapper * @see #DEFAULT_KEYS_SEARCH_GLOBAL * @see #onKeyDown */ - public final void setDefaultKeyMode(int mode) { + public final void setDefaultKeyMode(@DefaultKeyMode int mode) { mDefaultKeyMode = mode; // Some modes use a SpannableStringBuilder to track & dispatch input events @@ -2213,7 +2301,7 @@ public class Activity extends ContextThemeWrapper */ public void onBackPressed() { if (!mFragments.popBackStackImmediate()) { - finish(); + finishWithTransition(); } } @@ -2528,6 +2616,7 @@ public class Activity extends ContextThemeWrapper * simply returns null so that all panel sub-windows will have the default * menu behavior. */ + @Nullable public View onCreatePanelView(int featureId) { return null; } @@ -2574,7 +2663,7 @@ public class Activity extends ContextThemeWrapper */ public boolean onMenuOpened(int featureId, Menu menu) { if (featureId == Window.FEATURE_ACTION_BAR) { - initActionBar(); + initWindowDecorActionBar(); if (mActionBar != null) { mActionBar.dispatchMenuVisibilityChanged(true); } else { @@ -2655,7 +2744,7 @@ public class Activity extends ContextThemeWrapper break; case Window.FEATURE_ACTION_BAR: - initActionBar(); + initWindowDecorActionBar(); mActionBar.dispatchMenuVisibilityChanged(false); break; } @@ -3025,6 +3114,7 @@ public class Activity extends ContextThemeWrapper * {@link FragmentManager} instead; this is also * available on older platforms through the Android compatibility package. */ + @Nullable @Deprecated protected Dialog onCreateDialog(int id, Bundle args) { return onCreateDialog(id); @@ -3112,6 +3202,7 @@ public class Activity extends ContextThemeWrapper * {@link FragmentManager} instead; this is also * available on older platforms through the Android compatibility package. */ + @Nullable @Deprecated public final boolean showDialog(int id, Bundle args) { if (mManagedDialogs == null) { @@ -3233,13 +3324,13 @@ public class Activity extends ContextThemeWrapper * <p>It is typically called from onSearchRequested(), either directly from * Activity.onSearchRequested() or from an overridden version in any given * Activity. If your goal is simply to activate search, it is preferred to call - * onSearchRequested(), which may have been overriden elsewhere in your Activity. If your goal + * onSearchRequested(), which may have been overridden elsewhere in your Activity. If your goal * is to inject specific data such as context data, it is preferred to <i>override</i> * onSearchRequested(), so that any callers to it will benefit from the override. * * @param initialQuery Any non-null non-empty string will be inserted as * pre-entered text in the search query box. - * @param selectInitialQuery If true, the intial query will be preselected, which means that + * @param selectInitialQuery If true, the initial query will be preselected, which means that * any further typing will replace it. This is useful for cases where an entire pre-formed * query is being inserted. If false, the selection point will be placed at the end of the * inserted query. This is useful when the inserted query is text that the user entered, @@ -3257,11 +3348,11 @@ public class Activity extends ContextThemeWrapper * @see android.app.SearchManager * @see #onSearchRequested */ - public void startSearch(String initialQuery, boolean selectInitialQuery, - Bundle appSearchData, boolean globalSearch) { + public void startSearch(@Nullable String initialQuery, boolean selectInitialQuery, + @Nullable Bundle appSearchData, boolean globalSearch) { ensureSearchManager(); mSearchManager.startSearch(initialQuery, selectInitialQuery, getComponentName(), - appSearchData, globalSearch); + appSearchData, globalSearch); } /** @@ -3274,7 +3365,7 @@ public class Activity extends ContextThemeWrapper * searches. This data will be returned with SEARCH intent(s). Null if * no extra data is required. */ - public void triggerSearch(String query, Bundle appSearchData) { + public void triggerSearch(String query, @Nullable Bundle appSearchData) { ensureSearchManager(); mSearchManager.triggerSearch(query, getComponentName(), appSearchData); } @@ -3341,6 +3432,7 @@ public class Activity extends ContextThemeWrapper * Convenience for calling * {@link android.view.Window#getLayoutInflater}. */ + @NonNull public LayoutInflater getLayoutInflater() { return getWindow().getLayoutInflater(); } @@ -3348,10 +3440,11 @@ public class Activity extends ContextThemeWrapper /** * Returns a {@link MenuInflater} with this context. */ + @NonNull public MenuInflater getMenuInflater() { // Make sure that action views can get an appropriate theme. if (mMenuInflater == null) { - initActionBar(); + initWindowDecorActionBar(); if (mActionBar != null) { mMenuInflater = new MenuInflater(mActionBar.getThemedContext(), this); } else { @@ -3386,16 +3479,20 @@ public class Activity extends ContextThemeWrapper * * @throws android.content.ActivityNotFoundException * - * @see #startActivity + * @see #startActivity */ public void startActivityForResult(Intent intent, int requestCode) { - startActivityForResult(intent, requestCode, null); + Bundle options = null; + if (mWindow.hasFeature(Window.FEATURE_CONTENT_TRANSITIONS)) { + options = ActivityOptions.makeSceneTransitionAnimation().toBundle(); + } + startActivityForResult(intent, requestCode, options); } /** * Launch an activity for which you would like a result when it finished. * When this activity exits, your - * onActivityResult() method will be called with the given requestCode. + * onActivityResult() method will be called with the given requestCode. * Using a negative requestCode is the same as calling * {@link #startActivity} (the activity is not launched as a sub-activity). * @@ -3408,9 +3505,9 @@ public class Activity extends ContextThemeWrapper * * <p>As a special case, if you call startActivityForResult() with a requestCode * >= 0 during the initial onCreate(Bundle savedInstanceState)/onResume() of your - * activity, then your window will not be displayed until a result is - * returned back from the started activity. This is to avoid visible - * flickering when redirecting to another activity. + * activity, then your window will not be displayed until a result is + * returned back from the started activity. This is to avoid visible + * flickering when redirecting to another activity. * * <p>This method throws {@link android.content.ActivityNotFoundException} * if there was no Activity found to run the given Intent. @@ -3424,9 +3521,20 @@ public class Activity extends ContextThemeWrapper * * @throws android.content.ActivityNotFoundException * - * @see #startActivity + * @see #startActivity */ - public void startActivityForResult(Intent intent, int requestCode, Bundle options) { + public void startActivityForResult(Intent intent, int requestCode, @Nullable Bundle options) { + if (options != null) { + ActivityOptions activityOptions = new ActivityOptions(options); + if (activityOptions.getAnimationType() == ActivityOptions.ANIM_SCENE_TRANSITION) { + if (mActionBar != null) { + ArrayMap<String, View> sharedElementMap = new ArrayMap<String, View>(); + mActionBar.captureSharedElements(sharedElementMap); + activityOptions.addSharedElements(sharedElementMap); + } + options = mWindow.startExitTransitionToCallee(options); + } + } if (mParent == null) { Instrumentation.ActivityResult ar = mInstrumentation.execStartActivity( @@ -3505,7 +3613,7 @@ public class Activity extends ContextThemeWrapper * @param extraFlags Always set to 0. */ public void startIntentSenderForResult(IntentSender intent, int requestCode, - Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags) + @Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags) throws IntentSender.SendIntentException { startIntentSenderForResult(intent, requestCode, fillInIntent, flagsMask, flagsValues, extraFlags, null); @@ -3537,7 +3645,7 @@ public class Activity extends ContextThemeWrapper * override any that conflict with those given by the IntentSender. */ public void startIntentSenderForResult(IntentSender intent, int requestCode, - Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags, + @Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags, Bundle options) throws IntentSender.SendIntentException { if (mParent == null) { startIntentSenderForResultInner(intent, requestCode, fillInIntent, @@ -3599,7 +3707,7 @@ public class Activity extends ContextThemeWrapper */ @Override public void startActivity(Intent intent) { - startActivity(intent, null); + this.startActivity(intent, null); } /** @@ -3625,7 +3733,7 @@ public class Activity extends ContextThemeWrapper * @see #startActivityForResult */ @Override - public void startActivity(Intent intent, Bundle options) { + public void startActivity(Intent intent, @Nullable Bundle options) { if (options != null) { startActivityForResult(intent, -1, options); } else { @@ -3674,7 +3782,7 @@ public class Activity extends ContextThemeWrapper * @see #startActivityForResult */ @Override - public void startActivities(Intent[] intents, Bundle options) { + public void startActivities(Intent[] intents, @Nullable Bundle options) { mInstrumentation.execStartActivities(this, mMainThread.getApplicationThread(), mToken, this, intents, options); } @@ -3693,7 +3801,7 @@ public class Activity extends ContextThemeWrapper * @param extraFlags Always set to 0. */ public void startIntentSender(IntentSender intent, - Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags) + @Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags) throws IntentSender.SendIntentException { startIntentSender(intent, fillInIntent, flagsMask, flagsValues, extraFlags, null); @@ -3720,7 +3828,7 @@ public class Activity extends ContextThemeWrapper * override any that conflict with those given by the IntentSender. */ public void startIntentSender(IntentSender intent, - Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags, + @Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags, Bundle options) throws IntentSender.SendIntentException { if (options != null) { startIntentSenderForResult(intent, -1, fillInIntent, flagsMask, @@ -3748,7 +3856,7 @@ public class Activity extends ContextThemeWrapper * @see #startActivity * @see #startActivityForResult */ - public boolean startActivityIfNeeded(Intent intent, int requestCode) { + public boolean startActivityIfNeeded(@NonNull Intent intent, int requestCode) { return startActivityIfNeeded(intent, requestCode, null); } @@ -3782,7 +3890,8 @@ public class Activity extends ContextThemeWrapper * @see #startActivity * @see #startActivityForResult */ - public boolean startActivityIfNeeded(Intent intent, int requestCode, Bundle options) { + public boolean startActivityIfNeeded(@NonNull Intent intent, int requestCode, + @Nullable Bundle options) { if (mParent == null) { int result = ActivityManager.START_RETURN_INTENT_TO_CALLER; try { @@ -3831,7 +3940,7 @@ public class Activity extends ContextThemeWrapper * wasn't. In general, if true is returned you will then want to call * finish() on yourself. */ - public boolean startNextMatchingActivity(Intent intent) { + public boolean startNextMatchingActivity(@NonNull Intent intent) { return startNextMatchingActivity(intent, null); } @@ -3854,7 +3963,7 @@ public class Activity extends ContextThemeWrapper * wasn't. In general, if true is returned you will then want to call * finish() on yourself. */ - public boolean startNextMatchingActivity(Intent intent, Bundle options) { + public boolean startNextMatchingActivity(@NonNull Intent intent, @Nullable Bundle options) { if (mParent == null) { try { intent.migrateExtraStreamToClipData(); @@ -3884,7 +3993,7 @@ public class Activity extends ContextThemeWrapper * @see #startActivity * @see #startActivityForResult */ - public void startActivityFromChild(Activity child, Intent intent, + public void startActivityFromChild(@NonNull Activity child, Intent intent, int requestCode) { startActivityFromChild(child, intent, requestCode, null); } @@ -3908,8 +4017,8 @@ public class Activity extends ContextThemeWrapper * @see #startActivity * @see #startActivityForResult */ - public void startActivityFromChild(Activity child, Intent intent, - int requestCode, Bundle options) { + public void startActivityFromChild(@NonNull Activity child, Intent intent, + int requestCode, @Nullable Bundle options) { Instrumentation.ActivityResult ar = mInstrumentation.execStartActivity( this, mMainThread.getApplicationThread(), mToken, child, @@ -3934,7 +4043,7 @@ public class Activity extends ContextThemeWrapper * @see Fragment#startActivity * @see Fragment#startActivityForResult */ - public void startActivityFromFragment(Fragment fragment, Intent intent, + public void startActivityFromFragment(@NonNull Fragment fragment, Intent intent, int requestCode) { startActivityFromFragment(fragment, intent, requestCode, null); } @@ -3959,8 +4068,8 @@ public class Activity extends ContextThemeWrapper * @see Fragment#startActivity * @see Fragment#startActivityForResult */ - public void startActivityFromFragment(Fragment fragment, Intent intent, - int requestCode, Bundle options) { + public void startActivityFromFragment(@NonNull Fragment fragment, Intent intent, + int requestCode, @Nullable Bundle options) { Instrumentation.ActivityResult ar = mInstrumentation.execStartActivity( this, mMainThread.getApplicationThread(), mToken, fragment, @@ -3992,7 +4101,7 @@ public class Activity extends ContextThemeWrapper */ public void startIntentSenderFromChild(Activity child, IntentSender intent, int requestCode, Intent fillInIntent, int flagsMask, int flagsValues, - int extraFlags, Bundle options) + int extraFlags, @Nullable Bundle options) throws IntentSender.SendIntentException { startIntentSenderForResultInner(intent, requestCode, fillInIntent, flagsMask, flagsValues, child, options); @@ -4091,6 +4200,7 @@ public class Activity extends ContextThemeWrapper * @return The package of the activity that will receive your * reply, or null if none. */ + @Nullable public String getCallingPackage() { try { return ActivityManagerNative.getDefault().getCallingPackage(mToken); @@ -4113,6 +4223,7 @@ public class Activity extends ContextThemeWrapper * @return The ComponentName of the activity that will receive your * reply, or null if none. */ + @Nullable public ComponentName getCallingActivity() { try { return ActivityManagerNative.getDefault().getCallingActivity(mToken); @@ -4276,6 +4387,23 @@ public class Activity extends ContextThemeWrapper } /** + * Reverses the Activity Scene entry Transition and triggers the calling Activity + * to reverse its exit Transition. When the exit Transition completes, + * {@link #finish()} is called. If no entry Transition was used, finish() is called + * immediately and the Activity exit Transition is run. + * @see android.view.Window#setTriggerEarlySceneTransition(boolean, boolean) + * @see android.app.ActivityOptions#makeSceneTransitionAnimation(android.view.View, String) + */ + public void finishWithTransition() { + mWindow.startExitTransitionToCaller(new Runnable() { + @Override + public void run() { + finish(); + } + }); + } + + /** * Force finish another activity that you had previously started with * {@link #startActivityForResult}. * @@ -4305,7 +4433,7 @@ public class Activity extends ContextThemeWrapper * @param requestCode Request code that had been used to start the * activity. */ - public void finishActivityFromChild(Activity child, int requestCode) { + public void finishActivityFromChild(@NonNull Activity child, int requestCode) { try { ActivityManagerNative.getDefault() .finishSubActivity(mToken, child.mEmbeddedID, requestCode); @@ -4366,8 +4494,8 @@ public class Activity extends ContextThemeWrapper * * @see PendingIntent */ - public PendingIntent createPendingResult(int requestCode, Intent data, - int flags) { + public PendingIntent createPendingResult(int requestCode, @NonNull Intent data, + @PendingIntent.Flags int flags) { String packageName = getPackageName(); try { data.prepareToLeaveProcess(); @@ -4394,7 +4522,7 @@ public class Activity extends ContextThemeWrapper * @param requestedOrientation An orientation constant as used in * {@link ActivityInfo#screenOrientation ActivityInfo.screenOrientation}. */ - public void setRequestedOrientation(int requestedOrientation) { + public void setRequestedOrientation(@ActivityInfo.ScreenOrientation int requestedOrientation) { if (mParent == null) { try { ActivityManagerNative.getDefault().setRequestedOrientation( @@ -4416,6 +4544,7 @@ public class Activity extends ContextThemeWrapper * @return Returns an orientation constant as used in * {@link ActivityInfo#screenOrientation ActivityInfo.screenOrientation}. */ + @ActivityInfo.ScreenOrientation public int getRequestedOrientation() { if (mParent == null) { try { @@ -4487,6 +4616,7 @@ public class Activity extends ContextThemeWrapper * * @return The local class name. */ + @NonNull public String getLocalClassName() { final String pkg = getPackageName(); final String cls = mComponent.getClassName(); @@ -4532,9 +4662,9 @@ public class Activity extends ContextThemeWrapper mSearchManager = new SearchManager(this, null); } - + @Override - public Object getSystemService(String name) { + public Object getSystemService(@ServiceName @NonNull String name) { if (getBaseContext() == null) { throw new IllegalStateException( "System services not available to Activities before onCreate()"); @@ -4574,6 +4704,17 @@ public class Activity extends ContextThemeWrapper setTitle(getText(titleId)); } + /** + * Change the color of the title associated with this activity. + * <p> + * This method is deprecated starting in API Level 11 and replaced by action + * bar styles. For information on styling the Action Bar, read the <a + * href="{@docRoot} guide/topics/ui/actionbar.html">Action Bar</a> developer + * guide. + * + * @deprecated Use action bar styles instead. + */ + @Deprecated public void setTitleColor(int textColor) { mTitleColor = textColor; onTitleChanged(mTitle, textColor); @@ -4603,6 +4744,36 @@ public class Activity extends ContextThemeWrapper } /** + * Set a label and icon to be used in the Recents task display. When {@link + * ActivityManager#getRecentTasks} is called, the activities of each task are + * traversed in order from the topmost activity to the bottommost. As soon as one activity is + * found with either a non-null label or a non-null icon set by this call the traversal is + * ended. For each task those values will be returned in {@link + * ActivityManager.RecentTaskInfo#activityLabel} and {@link + * ActivityManager.RecentTaskInfo#activityIcon}. + * + * @see ActivityManager#getRecentTasks + * @see ActivityManager.RecentTaskInfo + * + * @param activityLabel The label to use in the RecentTaskInfo. + * @param activityIcon The Bitmap to use in the RecentTaskInfo. + */ + public void setActivityLabelAndIcon(CharSequence activityLabel, Bitmap activityIcon) { + final Bitmap scaledIcon; + if (activityIcon != null) { + final int size = ActivityManager.getLauncherLargeIconSizeInner(this); + scaledIcon = Bitmap.createScaledBitmap(activityIcon, size, size, true); + } else { + scaledIcon = null; + } + try { + ActivityManagerNative.getDefault().setActivityLabelAndIcon(mToken, activityLabel, + scaledIcon); + } catch (RemoteException e) { + } + } + + /** * Sets the visibility of the progress bar in the title. * <p> * In order for the progress bar to be shown, the feature must be requested @@ -4639,7 +4810,8 @@ public class Activity extends ContextThemeWrapper */ public final void setProgressBarIndeterminate(boolean indeterminate) { getWindow().setFeatureInt(Window.FEATURE_PROGRESS, - indeterminate ? Window.PROGRESS_INDETERMINATE_ON : Window.PROGRESS_INDETERMINATE_OFF); + indeterminate ? Window.PROGRESS_INDETERMINATE_ON + : Window.PROGRESS_INDETERMINATE_OFF); } /** @@ -4696,7 +4868,7 @@ public class Activity extends ContextThemeWrapper /** * Gets the suggested audio stream whose volume should be changed by the - * harwdare volume controls. + * hardware volume controls. * * @return The suggested audio stream type whose volume should be changed by * the hardware volume controls. @@ -4732,6 +4904,7 @@ public class Activity extends ContextThemeWrapper * @see android.view.LayoutInflater#createView * @see android.view.Window#getLayoutInflater */ + @Nullable public View onCreateView(String name, Context context, AttributeSet attrs) { return null; } @@ -4990,6 +5163,7 @@ public class Activity extends ContextThemeWrapper * * @see ActionMode */ + @Nullable public ActionMode startActionMode(ActionMode.Callback callback) { return mWindow.getDecorView().startActionMode(callback); } @@ -5005,9 +5179,10 @@ public class Activity extends ContextThemeWrapper * @return The new action mode, or <code>null</code> if the activity does not want to * provide special handling for this action mode. (It will be handled by the system.) */ + @Nullable @Override public ActionMode onWindowStartingActionMode(ActionMode.Callback callback) { - initActionBar(); + initWindowDecorActionBar(); if (mActionBar != null) { return mActionBar.startActionMode(callback); } @@ -5148,6 +5323,7 @@ public class Activity extends ContextThemeWrapper * @return a new Intent targeting the defined parent of this activity or null if * there is no valid parent. */ + @Nullable public Intent getParentActivityIntent() { final String parentName = mActivityInfo.parentActivityName; if (TextUtils.isEmpty(parentName)) { @@ -5190,6 +5366,16 @@ public class Activity extends ContextThemeWrapper CharSequence title, Activity parent, String id, NonConfigurationInstances lastNonConfigurationInstances, Configuration config) { + attach(context, aThread, instr, token, ident, application, intent, info, title, parent, id, + lastNonConfigurationInstances, config, null); + } + + final void attach(Context context, ActivityThread aThread, + Instrumentation instr, IBinder token, int ident, + Application application, Intent intent, ActivityInfo info, + CharSequence title, Activity parent, String id, + NonConfigurationInstances lastNonConfigurationInstances, + Configuration config, Bundle options) { attachBaseContext(context); mFragments.attachActivity(this, mContainer, null); @@ -5204,7 +5390,7 @@ public class Activity extends ContextThemeWrapper mWindow.setUiOptions(info.uiOptions); } mUiThread = Thread.currentThread(); - + mMainThread = aThread; mInstrumentation = instr; mToken = token; @@ -5227,6 +5413,34 @@ public class Activity extends ContextThemeWrapper } mWindowManager = mWindow.getWindowManager(); mCurrentConfig = config; + Window.SceneTransitionListener sceneTransitionListener + = new Window.SceneTransitionListener() { + @Override + public void nullPendingTransition() { + overridePendingTransition(0, 0); + } + + @Override + public void convertFromTranslucent() { + Activity.this.convertFromTranslucent(); + } + + @Override + public void convertToTranslucent() { + Activity.this.convertToTranslucent(null); + } + + @Override + public void sharedElementStart(Transition transition) { + Activity.this.onCaptureSharedElementStart(transition); + } + + @Override + public void sharedElementEnd() { + Activity.this.onCaptureSharedElementEnd(); + } + }; + mWindow.setTransitionOptions(options, sceneTransitionListener); } /** @hide */ @@ -5240,7 +5454,7 @@ public class Activity extends ContextThemeWrapper com.android.internal.R.styleable.Window_windowNoDisplay, false); mFragments.dispatchActivityCreated(); } - + final void performStart() { mFragments.noteStateNotSaved(); mCalled = false; @@ -5397,7 +5611,7 @@ public class Activity extends ContextThemeWrapper } } } - + mStopped = true; } mResumed = false; @@ -5412,7 +5626,27 @@ public class Activity extends ContextThemeWrapper mLoaderManager.doDestroy(); } } - + + /** + * Called when setting up Activity Scene transitions when the start state for shared + * elements has been captured. Override this method to modify the start position of shared + * elements for the entry Transition. + * + * @param transition The <code>Transition</code> being used to change + * bounds of shared elements in the source Activity to + * the bounds defined by the entering Scene. + */ + public void onCaptureSharedElementStart(Transition transition) { + } + + /** + * Called when setting up Activity Scene transitions when the final state for + * shared elements state has been captured. Override this method to modify the destination + * position of shared elements for the entry Transition. + */ + public void onCaptureSharedElementEnd() { + } + /** * @hide */ @@ -5420,7 +5654,7 @@ public class Activity extends ContextThemeWrapper return mResumed; } - void dispatchActivityResult(String who, int requestCode, + void dispatchActivityResult(String who, int requestCode, int resultCode, Intent data) { if (false) Log.v( TAG, "Dispatching result: who=" + who + ", reqCode=" + requestCode @@ -5436,6 +5670,22 @@ public class Activity extends ContextThemeWrapper } } + /** @hide */ + public void startLockTask() { + try { + ActivityManagerNative.getDefault().startLockTaskMode(mToken); + } catch (RemoteException e) { + } + } + + /** @hide */ + public void stopLockTask() { + try { + ActivityManagerNative.getDefault().stopLockTaskMode(); + } catch (RemoteException e) { + } + } + /** * Interface for informing a translucent {@link Activity} once all visible activities below it * have completed drawing. This is necessary only after an {@link Activity} has been made diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index c877cd3..c027e99 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -155,6 +155,13 @@ public class ActivityManager { public static final int START_SWITCHES_CANCELED = 4; /** + * Result for IActivityManaqer.startActivity: a new activity was attempted to be started + * while in Lock Task Mode. + * @hide + */ + public static final int START_RETURN_LOCK_TASK_MODE_VIOLATION = 5; + + /** * Flag for IActivityManaqer.startActivity: do special start mode where * a new activity is launched only if it is needed. * @hide @@ -502,6 +509,24 @@ public class ActivityManager { */ public int stackId; + /** + * The id of the user the task was running as. + * @hide + */ + public int userId; + + /** + * The label of the highest activity in the task stack to have set a label using + * {@link Activity#setActivityLabelAndIcon(CharSequence, android.graphics.Bitmap)}. + */ + public CharSequence activityLabel; + + /** + * The Bitmap icon of the highest activity in the task stack to set a Bitmap using + * {@link Activity#setActivityLabelAndIcon(CharSequence, android.graphics.Bitmap)}. + */ + public Bitmap activityIcon; + public RecentTaskInfo() { } @@ -523,20 +548,28 @@ public class ActivityManager { ComponentName.writeToParcel(origActivity, dest); TextUtils.writeToParcel(description, dest, Parcelable.PARCELABLE_WRITE_RETURN_VALUE); + TextUtils.writeToParcel(activityLabel, dest, + Parcelable.PARCELABLE_WRITE_RETURN_VALUE); + if (activityIcon == null) { + dest.writeInt(0); + } else { + dest.writeInt(1); + activityIcon.writeToParcel(dest, 0); + } dest.writeInt(stackId); + dest.writeInt(userId); } public void readFromParcel(Parcel source) { id = source.readInt(); persistentId = source.readInt(); - if (source.readInt() != 0) { - baseIntent = Intent.CREATOR.createFromParcel(source); - } else { - baseIntent = null; - } + baseIntent = source.readInt() > 0 ? Intent.CREATOR.createFromParcel(source) : null; origActivity = ComponentName.readFromParcel(source); description = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); + activityLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); + activityIcon = source.readInt() > 0 ? Bitmap.CREATOR.createFromParcel(source) : null; stackId = source.readInt(); + userId = source.readInt(); } public static final Creator<RecentTaskInfo> CREATOR @@ -560,7 +593,7 @@ public class ActivityManager { * {@link android.content.Intent#FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS} flag. */ public static final int RECENT_WITH_EXCLUDED = 0x0001; - + /** * Provides a list that does not contain any * recent tasks that currently are not available to the user. @@ -568,6 +601,13 @@ public class ActivityManager { public static final int RECENT_IGNORE_UNAVAILABLE = 0x0002; /** + * Provides a list that contains recent tasks for all + * profiles of a user. + * @hide + */ + public static final int RECENT_INCLUDE_PROFILES = 0x0004; + + /** * Return a list of the tasks that the user has recently launched, with * the most recent being first and older ones after in order. * @@ -933,6 +973,16 @@ public class ActivityManager { } } + /** @hide */ + public boolean isInHomeStack(int taskId) { + try { + return ActivityManagerNative.getDefault().isInHomeStack(taskId); + } catch (RemoteException e) { + // System dead, we will be dead too soon! + return false; + } + } + /** * Flag for {@link #moveTaskToFront(int, int)}: also move the "home" * activity along with the task, so it is positioned immediately behind @@ -1938,7 +1988,11 @@ public class ActivityManager { * @return dimensions of square icons in terms of pixels */ public int getLauncherLargeIconSize() { - final Resources res = mContext.getResources(); + return getLauncherLargeIconSizeInner(mContext); + } + + static int getLauncherLargeIconSizeInner(Context context) { + final Resources res = context.getResources(); final int size = res.getDimensionPixelSize(android.R.dimen.app_icon_size); final int sw = res.getConfiguration().smallestScreenWidthDp; @@ -2222,4 +2276,35 @@ public class ActivityManager { e.printStackTrace(pw); } } + + /** + * @hide + */ + public void startLockTaskMode(int taskId) { + try { + ActivityManagerNative.getDefault().startLockTaskMode(taskId); + } catch (RemoteException e) { + } + } + + /** + * @hide + */ + public void stopLockTaskMode() { + try { + ActivityManagerNative.getDefault().stopLockTaskMode(); + } catch (RemoteException e) { + } + } + + /** + * @hide + */ + public boolean isInLockTaskMode() { + try { + return ActivityManagerNative.getDefault().isInLockTaskMode(); + } catch (RemoteException e) { + return false; + } + } } diff --git a/core/java/android/app/ActivityManagerNative.java b/core/java/android/app/ActivityManagerNative.java index 14c495f..44c74d8 100644 --- a/core/java/android/app/ActivityManagerNative.java +++ b/core/java/android/app/ActivityManagerNative.java @@ -101,9 +101,9 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM } } - static public void noteWakeupAlarm(PendingIntent ps) { + static public void noteWakeupAlarm(PendingIntent ps, int sourceUid, String sourcePkg) { try { - getDefault().noteWakeupAlarm(ps.getTarget()); + getDefault().noteWakeupAlarm(ps.getTarget(), sourceUid, sourcePkg); } catch (RemoteException ex) { } } @@ -654,6 +654,15 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM return true; } + case IS_IN_HOME_STACK_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + int taskId = data.readInt(); + boolean isInHomeStack = isInHomeStack(taskId); + reply.writeNoException(); + reply.writeInt(isInHomeStack ? 1 : 0); + return true; + } + case SET_FOCUSED_STACK_TRANSACTION: { data.enforceInterface(IActivityManager.descriptor); int stackId = data.readInt(); @@ -1244,7 +1253,9 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM data.enforceInterface(IActivityManager.descriptor); IIntentSender is = IIntentSender.Stub.asInterface( data.readStrongBinder()); - noteWakeupAlarm(is); + int sourceUid = data.readInt(); + String sourcePkg = data.readString(); + noteWakeupAlarm(is, sourceUid, sourcePkg); reply.writeNoException(); return true; } @@ -1686,6 +1697,15 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM return true; } + case START_USER_IN_BACKGROUND_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + int userid = data.readInt(); + boolean result = startUserInBackground(userid); + reply.writeNoException(); + reply.writeInt(result ? 1 : 0); + return true; + } + case STOP_USER_TRANSACTION: { data.enforceInterface(IActivityManager.descriptor); int userid = data.readInt(); @@ -1816,6 +1836,17 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM return true; } + case GET_TAG_FOR_INTENT_SENDER_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + IIntentSender r = IIntentSender.Stub.asInterface( + data.readStrongBinder()); + String prefix = data.readString(); + String tag = getTagForIntentSender(r, prefix); + reply.writeNoException(); + reply.writeString(tag); + return true; + } + case UPDATE_PERSISTENT_CONFIGURATION_TRANSACTION: { data.enforceInterface(IActivityManager.descriptor); Configuration config = Configuration.CREATOR.createFromParcel(data); @@ -2041,6 +2072,48 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM reply.writeStrongBinder(homeActivityToken); return true; } + + case START_LOCK_TASK_BY_TASK_ID_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + final int taskId = data.readInt(); + startLockTaskMode(taskId); + reply.writeNoException(); + return true; + } + + case START_LOCK_TASK_BY_TOKEN_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + IBinder token = data.readStrongBinder(); + startLockTaskMode(token); + reply.writeNoException(); + return true; + } + + case STOP_LOCK_TASK_MODE_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + stopLockTaskMode(); + reply.writeNoException(); + return true; + } + + case IS_IN_LOCK_TASK_MODE_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + final boolean isInLockTaskMode = isInLockTaskMode(); + reply.writeNoException(); + reply.writeInt(isInLockTaskMode ? 1 : 0); + return true; + } + + case SET_ACTIVITY_LABEL_ICON_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + IBinder token = data.readStrongBinder(); + CharSequence activityLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(data); + Bitmap activityIcon = data.readInt() > 0 + ? Bitmap.CREATOR.createFromParcel(data) : null; + setActivityLabelAndIcon(token, activityLabel, activityIcon); + reply.writeNoException(); + return true; + } } return super.onTransact(code, data, reply, flags); @@ -2786,6 +2859,19 @@ class ActivityManagerProxy implements IActivityManager return info; } @Override + public boolean isInHomeStack(int taskId) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeInt(taskId); + mRemote.transact(IS_IN_HOME_STACK_TRANSACTION, data, reply, 0); + reply.readException(); + boolean isInHomeStack = reply.readInt() > 0; + data.recycle(); + reply.recycle(); + return isInHomeStack; + } + @Override public void setFocusedStack(int stackId) throws RemoteException { Parcel data = Parcel.obtain(); @@ -3615,10 +3701,13 @@ class ActivityManagerProxy implements IActivityManager mRemote.transact(ENTER_SAFE_MODE_TRANSACTION, data, null, 0); data.recycle(); } - public void noteWakeupAlarm(IIntentSender sender) throws RemoteException { + public void noteWakeupAlarm(IIntentSender sender, int sourceUid, String sourcePkg) + throws RemoteException { Parcel data = Parcel.obtain(); - data.writeStrongBinder(sender.asBinder()); data.writeInterfaceToken(IActivityManager.descriptor); + data.writeStrongBinder(sender.asBinder()); + data.writeInt(sourceUid); + data.writeString(sourcePkg); mRemote.transact(NOTE_WAKEUP_ALARM_TRANSACTION, data, null, 0); data.recycle(); } @@ -4229,6 +4318,19 @@ class ActivityManagerProxy implements IActivityManager return result; } + public boolean startUserInBackground(int userid) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeInt(userid); + mRemote.transact(START_USER_IN_BACKGROUND_TRANSACTION, data, reply, 0); + reply.readException(); + boolean result = reply.readInt() != 0; + reply.recycle(); + data.recycle(); + return result; + } + public int stopUser(int userid, IStopUserCallback callback) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); @@ -4371,6 +4473,21 @@ class ActivityManagerProxy implements IActivityManager return res; } + public String getTagForIntentSender(IIntentSender sender, String prefix) + throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeStrongBinder(sender.asBinder()); + data.writeString(prefix); + mRemote.transact(GET_TAG_FOR_INTENT_SENDER_TRANSACTION, data, reply, 0); + reply.readException(); + String res = reply.readString(); + data.recycle(); + reply.recycle(); + return res; + } + public void updatePersistentConfiguration(Configuration values) throws RemoteException { Parcel data = Parcel.obtain(); @@ -4686,5 +4803,74 @@ class ActivityManagerProxy implements IActivityManager return res; } + @Override + public void startLockTaskMode(int taskId) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeInt(taskId); + mRemote.transact(START_LOCK_TASK_BY_TASK_ID_TRANSACTION, data, reply, 0); + reply.readException(); + data.recycle(); + reply.recycle(); + } + + @Override + public void startLockTaskMode(IBinder token) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeStrongBinder(token); + mRemote.transact(START_LOCK_TASK_BY_TOKEN_TRANSACTION, data, reply, 0); + reply.readException(); + data.recycle(); + reply.recycle(); + } + + @Override + public void stopLockTaskMode() throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + mRemote.transact(STOP_LOCK_TASK_MODE_TRANSACTION, data, reply, 0); + reply.readException(); + data.recycle(); + reply.recycle(); + } + + @Override + public boolean isInLockTaskMode() throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + mRemote.transact(IS_IN_LOCK_TASK_MODE_TRANSACTION, data, reply, 0); + reply.readException(); + boolean isInLockTaskMode = reply.readInt() == 1; + data.recycle(); + reply.recycle(); + return isInLockTaskMode; + } + + @Override + public void setActivityLabelAndIcon(IBinder token, CharSequence activityLabel, + Bitmap activityIcon) throws RemoteException + { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeStrongBinder(token); + TextUtils.writeToParcel(activityLabel, data, 0); + if (activityIcon != null) { + data.writeInt(1); + activityIcon.writeToParcel(data, 0); + } else { + data.writeInt(0); + } + mRemote.transact(SET_ACTIVITY_LABEL_ICON_TRANSACTION, data, reply, IBinder.FLAG_ONEWAY); + reply.readException(); + data.recycle(); + reply.recycle(); + } + private IBinder mRemote; } diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index 87b1e24..4384580 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -20,16 +20,26 @@ import android.content.Context; import android.graphics.Bitmap; import android.os.Bundle; import android.os.Handler; +import android.os.IBinder; import android.os.IRemoteCallback; import android.os.RemoteException; +import android.os.ResultReceiver; +import android.transition.Transition; +import android.util.Log; +import android.util.Pair; import android.view.View; +import java.util.ArrayList; +import java.util.Map; + /** * Helper class for building an options Bundle that can be used with * {@link android.content.Context#startActivity(android.content.Intent, android.os.Bundle) * Context.startActivity(Intent, Bundle)} and related methods. */ public class ActivityOptions { + private static final String TAG = "ActivityOptions"; + /** * The package name that created the options. * @hide @@ -90,6 +100,31 @@ public class ActivityOptions { */ public static final String KEY_ANIM_START_LISTENER = "android:animStartListener"; + /** + * For Activity transitions, the calling Activity's TransitionListener used to + * notify the called Activity when the shared element and the exit transitions + * complete. + */ + private static final String KEY_TRANSITION_COMPLETE_LISTENER + = "android:transitionCompleteListener"; + + /** + * For Activity transitions, the called Activity's listener to receive calls + * when transitions complete. + */ + private static final String KEY_TRANSITION_TARGET_LISTENER = "android:transitionTargetListener"; + + /** + * The names of shared elements that are transitioned to the started Activity. + * This is also the name of shared elements that the started Activity accepted. + */ + private static final String KEY_SHARED_ELEMENT_NAMES = "android:shared_element_names"; + + /** + * The shared elements names of the views in the calling Activity. + */ + private static final String KEY_LOCAL_ELEMENT_NAMES = "android:local_element_names"; + /** @hide */ public static final int ANIM_NONE = 0; /** @hide */ @@ -100,6 +135,13 @@ public class ActivityOptions { public static final int ANIM_THUMBNAIL_SCALE_UP = 3; /** @hide */ public static final int ANIM_THUMBNAIL_SCALE_DOWN = 4; + /** @hide */ + public static final int ANIM_SCENE_TRANSITION = 5; + + private static final int MSG_SET_LISTENER = 100; + private static final int MSG_HIDE_SHARED_ELEMENTS = 101; + private static final int MSG_PREPARE_RESTORE = 102; + private static final int MSG_RESTORE = 103; private String mPackageName; private int mAnimationType = ANIM_NONE; @@ -111,6 +153,9 @@ public class ActivityOptions { private int mStartWidth; private int mStartHeight; private IRemoteCallback mAnimationStartedListener; + private ResultReceiver mTransitionCompleteListener; + private ArrayList<String> mSharedElementNames; + private ArrayList<String> mLocalElementNames; /** * Create an ActivityOptions specifying a custom animation to run when @@ -156,11 +201,12 @@ public class ActivityOptions { opts.mAnimationType = ANIM_CUSTOM; opts.mCustomEnterResId = enterResId; opts.mCustomExitResId = exitResId; - opts.setListener(handler, listener); + opts.setOnAnimationStartedListener(handler, listener); return opts; } - private void setListener(Handler handler, OnAnimationStartedListener listener) { + private void setOnAnimationStartedListener(Handler handler, + OnAnimationStartedListener listener) { if (listener != null) { final Handler h = handler; final OnAnimationStartedListener finalListener = listener; @@ -185,6 +231,12 @@ public class ActivityOptions { void onAnimationStarted(); } + /** @hide */ + public interface ActivityTransitionTarget { + void sharedElementTransitionComplete(Bundle transitionArgs); + void exitTransitionComplete(); + } + /** * Create an ActivityOptions specifying an animation where the new * activity is scaled from a small originating area of the screen to @@ -298,7 +350,56 @@ public class ActivityOptions { source.getLocationOnScreen(pts); opts.mStartX = pts[0] + startX; opts.mStartY = pts[1] + startY; - opts.setListener(source.getHandler(), listener); + opts.setOnAnimationStartedListener(source.getHandler(), listener); + return opts; + } + + /** + * Create an ActivityOptions to transition between Activities using cross-Activity scene + * animations. This method carries the position of one shared element to the started Activity. + * + * <p>This requires {@link android.view.Window#FEATURE_CONTENT_TRANSITIONS} to be + * enabled on the calling Activity to cause an exit transition. The same must be in + * the called Activity to get an entering transition.</p> + * @param sharedElement The View to transition to the started Activity. sharedElement must + * have a non-null sharedElementName. + * @param sharedElementName The shared element name as used in the target Activity. This may + * be null if it has the same name as sharedElement. + * @return Returns a new ActivityOptions object that you can use to + * supply these options as the options Bundle when starting an activity. + */ + public static ActivityOptions makeSceneTransitionAnimation(View sharedElement, + String sharedElementName) { + return makeSceneTransitionAnimation( + new Pair<View, String>(sharedElement, sharedElementName)); + } + + /** + * Create an ActivityOptions to transition between Activities using cross-Activity scene + * animations. This method carries the position of multiple shared elements to the started + * Activity. + * + * <p>This requires {@link android.view.Window#FEATURE_CONTENT_TRANSITIONS} to be + * enabled on the calling Activity to cause an exit transition. The same must be in + * the called Activity to get an entering transition.</p> + * @param sharedElements The View to transition to the started Activity along with the + * shared element name as used in the started Activity. The view + * must have a non-null sharedElementName. + * @return Returns a new ActivityOptions object that you can use to + * supply these options as the options Bundle when starting an activity. + */ + public static ActivityOptions makeSceneTransitionAnimation( + Pair<View, String>... sharedElements) { + ActivityOptions opts = new ActivityOptions(); + opts.mAnimationType = ANIM_SCENE_TRANSITION; + opts.mSharedElementNames = new ArrayList<String>(); + opts.mLocalElementNames = new ArrayList<String>(); + + if (sharedElements != null) { + for (Pair<View, String> sharedElement : sharedElements) { + opts.addSharedElement(sharedElement.first, sharedElement.second); + } + } return opts; } @@ -309,23 +410,35 @@ public class ActivityOptions { public ActivityOptions(Bundle opts) { mPackageName = opts.getString(KEY_PACKAGE_NAME); mAnimationType = opts.getInt(KEY_ANIM_TYPE); - if (mAnimationType == ANIM_CUSTOM) { - mCustomEnterResId = opts.getInt(KEY_ANIM_ENTER_RES_ID, 0); - mCustomExitResId = opts.getInt(KEY_ANIM_EXIT_RES_ID, 0); - mAnimationStartedListener = IRemoteCallback.Stub.asInterface( - opts.getIBinder(KEY_ANIM_START_LISTENER)); - } else if (mAnimationType == ANIM_SCALE_UP) { - mStartX = opts.getInt(KEY_ANIM_START_X, 0); - mStartY = opts.getInt(KEY_ANIM_START_Y, 0); - mStartWidth = opts.getInt(KEY_ANIM_START_WIDTH, 0); - mStartHeight = opts.getInt(KEY_ANIM_START_HEIGHT, 0); - } else if (mAnimationType == ANIM_THUMBNAIL_SCALE_UP || - mAnimationType == ANIM_THUMBNAIL_SCALE_DOWN) { - mThumbnail = (Bitmap)opts.getParcelable(KEY_ANIM_THUMBNAIL); - mStartX = opts.getInt(KEY_ANIM_START_X, 0); - mStartY = opts.getInt(KEY_ANIM_START_Y, 0); - mAnimationStartedListener = IRemoteCallback.Stub.asInterface( - opts.getIBinder(KEY_ANIM_START_LISTENER)); + switch (mAnimationType) { + case ANIM_CUSTOM: + mCustomEnterResId = opts.getInt(KEY_ANIM_ENTER_RES_ID, 0); + mCustomExitResId = opts.getInt(KEY_ANIM_EXIT_RES_ID, 0); + mAnimationStartedListener = IRemoteCallback.Stub.asInterface( + opts.getBinder(KEY_ANIM_START_LISTENER)); + break; + + case ANIM_SCALE_UP: + mStartX = opts.getInt(KEY_ANIM_START_X, 0); + mStartY = opts.getInt(KEY_ANIM_START_Y, 0); + mStartWidth = opts.getInt(KEY_ANIM_START_WIDTH, 0); + mStartHeight = opts.getInt(KEY_ANIM_START_HEIGHT, 0); + break; + + case ANIM_THUMBNAIL_SCALE_UP: + case ANIM_THUMBNAIL_SCALE_DOWN: + mThumbnail = (Bitmap)opts.getParcelable(KEY_ANIM_THUMBNAIL); + mStartX = opts.getInt(KEY_ANIM_START_X, 0); + mStartY = opts.getInt(KEY_ANIM_START_Y, 0); + mAnimationStartedListener = IRemoteCallback.Stub.asInterface( + opts.getBinder(KEY_ANIM_START_LISTENER)); + break; + + case ANIM_SCENE_TRANSITION: + mTransitionCompleteListener = opts.getParcelable(KEY_TRANSITION_COMPLETE_LISTENER); + mSharedElementNames = opts.getStringArrayList(KEY_SHARED_ELEMENT_NAMES); + mLocalElementNames = opts.getStringArrayList(KEY_LOCAL_ELEMENT_NAMES); + break; } } @@ -380,6 +493,54 @@ public class ActivityOptions { } /** @hide */ + public ArrayList<String> getSharedElementNames() { return mSharedElementNames; } + + /** @hide */ + public ArrayList<String> getLocalElementNames() { return mLocalElementNames; } + + /** @hide */ + public void dispatchSceneTransitionStarted(final ActivityTransitionTarget target, + ArrayList<String> sharedElementNames) { + if (mTransitionCompleteListener != null) { + IRemoteCallback callback = new IRemoteCallback.Stub() { + @Override + public void sendResult(Bundle data) throws RemoteException { + if (data == null) { + target.exitTransitionComplete(); + } else { + target.sharedElementTransitionComplete(data); + } + } + }; + Bundle bundle = new Bundle(); + bundle.putBinder(KEY_TRANSITION_TARGET_LISTENER, callback.asBinder()); + bundle.putStringArrayList(KEY_SHARED_ELEMENT_NAMES, sharedElementNames); + mTransitionCompleteListener.send(MSG_SET_LISTENER, bundle); + } + } + + /** @hide */ + public void dispatchSharedElementsReady() { + if (mTransitionCompleteListener != null) { + mTransitionCompleteListener.send(MSG_HIDE_SHARED_ELEMENTS, null); + } + } + + /** @hide */ + public void dispatchPrepareRestore() { + if (mTransitionCompleteListener != null) { + mTransitionCompleteListener.send(MSG_PREPARE_RESTORE, null); + } + } + + /** @hide */ + public void dispatchRestore(Bundle sharedElementsArgs) { + if (mTransitionCompleteListener != null) { + mTransitionCompleteListener.send(MSG_RESTORE, sharedElementsArgs); + } + } + + /** @hide */ public void abort() { if (mAnimationStartedListener != null) { try { @@ -405,19 +566,22 @@ public class ActivityOptions { if (otherOptions.mPackageName != null) { mPackageName = otherOptions.mPackageName; } + mSharedElementNames = null; + mLocalElementNames = null; switch (otherOptions.mAnimationType) { case ANIM_CUSTOM: mAnimationType = otherOptions.mAnimationType; mCustomEnterResId = otherOptions.mCustomEnterResId; mCustomExitResId = otherOptions.mCustomExitResId; mThumbnail = null; - if (otherOptions.mAnimationStartedListener != null) { + if (mAnimationStartedListener != null) { try { - otherOptions.mAnimationStartedListener.sendResult(null); + mAnimationStartedListener.sendResult(null); } catch (RemoteException e) { } } mAnimationStartedListener = otherOptions.mAnimationStartedListener; + mTransitionCompleteListener = null; break; case ANIM_SCALE_UP: mAnimationType = otherOptions.mAnimationType; @@ -425,13 +589,14 @@ public class ActivityOptions { mStartY = otherOptions.mStartY; mStartWidth = otherOptions.mStartWidth; mStartHeight = otherOptions.mStartHeight; - if (otherOptions.mAnimationStartedListener != null) { + if (mAnimationStartedListener != null) { try { - otherOptions.mAnimationStartedListener.sendResult(null); + mAnimationStartedListener.sendResult(null); } catch (RemoteException e) { } } mAnimationStartedListener = null; + mTransitionCompleteListener = null; break; case ANIM_THUMBNAIL_SCALE_UP: case ANIM_THUMBNAIL_SCALE_DOWN: @@ -439,13 +604,22 @@ public class ActivityOptions { mThumbnail = otherOptions.mThumbnail; mStartX = otherOptions.mStartX; mStartY = otherOptions.mStartY; - if (otherOptions.mAnimationStartedListener != null) { + if (mAnimationStartedListener != null) { try { - otherOptions.mAnimationStartedListener.sendResult(null); + mAnimationStartedListener.sendResult(null); } catch (RemoteException e) { } } mAnimationStartedListener = otherOptions.mAnimationStartedListener; + mTransitionCompleteListener = null; + break; + case ANIM_SCENE_TRANSITION: + mAnimationType = otherOptions.mAnimationType; + mTransitionCompleteListener = otherOptions.mTransitionCompleteListener; + mThumbnail = null; + mAnimationStartedListener = null; + mSharedElementNames = otherOptions.mSharedElementNames; + mLocalElementNames = otherOptions.mLocalElementNames; break; } } @@ -468,7 +642,7 @@ public class ActivityOptions { b.putInt(KEY_ANIM_TYPE, mAnimationType); b.putInt(KEY_ANIM_ENTER_RES_ID, mCustomEnterResId); b.putInt(KEY_ANIM_EXIT_RES_ID, mCustomExitResId); - b.putIBinder(KEY_ANIM_START_LISTENER, mAnimationStartedListener + b.putBinder(KEY_ANIM_START_LISTENER, mAnimationStartedListener != null ? mAnimationStartedListener.asBinder() : null); break; case ANIM_SCALE_UP: @@ -484,10 +658,159 @@ public class ActivityOptions { b.putParcelable(KEY_ANIM_THUMBNAIL, mThumbnail); b.putInt(KEY_ANIM_START_X, mStartX); b.putInt(KEY_ANIM_START_Y, mStartY); - b.putIBinder(KEY_ANIM_START_LISTENER, mAnimationStartedListener + b.putBinder(KEY_ANIM_START_LISTENER, mAnimationStartedListener != null ? mAnimationStartedListener.asBinder() : null); break; + case ANIM_SCENE_TRANSITION: + b.putInt(KEY_ANIM_TYPE, mAnimationType); + if (mTransitionCompleteListener != null) { + b.putParcelable(KEY_TRANSITION_COMPLETE_LISTENER, mTransitionCompleteListener); + } + b.putStringArrayList(KEY_SHARED_ELEMENT_NAMES, mSharedElementNames); + b.putStringArrayList(KEY_LOCAL_ELEMENT_NAMES, mLocalElementNames); + break; } return b; } + + /** + * Return the filtered options only meant to be seen by the target activity itself + * @hide + */ + public ActivityOptions forTargetActivity() { + if (mAnimationType == ANIM_SCENE_TRANSITION) { + final ActivityOptions result = new ActivityOptions(); + result.update(this); + return result; + } + + return null; + } + + /** @hide */ + public void addSharedElements(Map<String, View> sharedElements) { + for (Map.Entry<String, View> entry : sharedElements.entrySet()) { + addSharedElement(entry.getValue(), entry.getKey()); + } + } + + /** @hide */ + public void updateSceneTransitionAnimation(Transition exitTransition, + Transition sharedElementTransition, SharedElementSource sharedElementSource) { + mTransitionCompleteListener = new ExitTransitionListener(exitTransition, + sharedElementTransition, sharedElementSource); + } + + private void addSharedElement(View view, String name) { + String sharedElementName = view.getSharedElementName(); + if (name == null) { + name = sharedElementName; + } + mSharedElementNames.add(name); + mLocalElementNames.add(sharedElementName); + } + + /** @hide */ + public interface SharedElementSource { + Bundle getSharedElementExitState(); + void acceptedSharedElements(ArrayList<String> sharedElementNames); + void hideSharedElements(); + void restore(Bundle sharedElementState); + void prepareForRestore(); + } + + private static class ExitTransitionListener extends ResultReceiver + implements Transition.TransitionListener { + private boolean mSharedElementNotified; + private IRemoteCallback mTransitionCompleteCallback; + private boolean mExitComplete; + private boolean mSharedElementComplete; + private SharedElementSource mSharedElementSource; + + public ExitTransitionListener(Transition exitTransition, Transition sharedElementTransition, + SharedElementSource sharedElementSource) { + super(null); + mSharedElementSource = sharedElementSource; + exitTransition.addListener(this); + sharedElementTransition.addListener(new Transition.TransitionListenerAdapter() { + @Override + public void onTransitionEnd(Transition transition) { + mSharedElementComplete = true; + notifySharedElement(); + transition.removeListener(this); + } + }); + } + + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + switch (resultCode) { + case MSG_SET_LISTENER: + IBinder listener = resultData.getBinder(KEY_TRANSITION_TARGET_LISTENER); + mTransitionCompleteCallback = IRemoteCallback.Stub.asInterface(listener); + ArrayList<String> sharedElementNames + = resultData.getStringArrayList(KEY_SHARED_ELEMENT_NAMES); + mSharedElementSource.acceptedSharedElements(sharedElementNames); + notifySharedElement(); + notifyExit(); + break; + case MSG_HIDE_SHARED_ELEMENTS: + mSharedElementSource.hideSharedElements(); + break; + case MSG_PREPARE_RESTORE: + mSharedElementSource.prepareForRestore(); + break; + case MSG_RESTORE: + mSharedElementSource.restore(resultData); + break; + } + } + + @Override + public void onTransitionStart(Transition transition) { + } + + @Override + public void onTransitionEnd(Transition transition) { + mExitComplete = true; + notifyExit(); + transition.removeListener(this); + } + + @Override + public void onTransitionCancel(Transition transition) { + onTransitionEnd(transition); + } + + @Override + public void onTransitionPause(Transition transition) { + } + + @Override + public void onTransitionResume(Transition transition) { + } + + private void notifySharedElement() { + if (!mSharedElementNotified && mSharedElementComplete + && mTransitionCompleteCallback != null) { + mSharedElementNotified = true; + try { + Bundle sharedElementState = mSharedElementSource.getSharedElementExitState(); + mTransitionCompleteCallback.sendResult(sharedElementState); + } catch (RemoteException e) { + Log.w(TAG, "Couldn't notify that the transition ended", e); + } + } + } + + private void notifyExit() { + if (mExitComplete && mTransitionCompleteCallback != null) { + try { + mTransitionCompleteCallback.sendResult(null); + } catch (RemoteException e) { + Log.w(TAG, "Couldn't notify that the transition ended", e); + } + } + } + } } diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 7f8dbba..965f815 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -56,6 +56,7 @@ import android.os.DropBoxManager; import android.os.Environment; import android.os.Handler; import android.os.IBinder; +import android.os.IRemoteCallback; import android.os.Looper; import android.os.Message; import android.os.MessageQueue; @@ -68,6 +69,8 @@ import android.os.SystemClock; import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; +import android.transition.Scene; +import android.transition.TransitionManager; import android.provider.Settings; import android.util.AndroidRuntimeException; import android.util.ArrayMap; @@ -75,6 +78,7 @@ import android.util.DisplayMetrics; import android.util.EventLog; import android.util.Log; import android.util.LogPrinter; +import android.util.Pair; import android.util.PrintWriterPrinter; import android.util.Slog; import android.util.SuperNotCalledException; @@ -289,6 +293,7 @@ public final class ActivityThread { boolean isForward; int pendingConfigChanges; boolean onlyLocalRequest; + Bundle activityOptions; View mPendingRemoveWindow; WindowManager mPendingRemoveWindowManager; @@ -581,9 +586,10 @@ public final class ActivityThread { } public final void scheduleResumeActivity(IBinder token, int processState, - boolean isForward) { + boolean isForward, Bundle resumeArgs) { updateProcessState(processState, false); - sendMessage(H.RESUME_ACTIVITY, token, isForward ? 1 : 0); + sendMessage(H.RESUME_ACTIVITY, new Pair<IBinder, Bundle>(token, resumeArgs), + isForward ? 1 : 0); } public final void scheduleSendResult(IBinder token, List<ResultInfo> results) { @@ -599,7 +605,8 @@ public final class ActivityThread { ActivityInfo info, Configuration curConfig, CompatibilityInfo compatInfo, int procState, Bundle state, List<ResultInfo> pendingResults, List<Intent> pendingNewIntents, boolean notResumed, boolean isForward, - String profileName, ParcelFileDescriptor profileFd, boolean autoStopProfiler) { + String profileName, ParcelFileDescriptor profileFd, boolean autoStopProfiler, + Bundle resumeArgs) { updateProcessState(procState, false); @@ -621,6 +628,7 @@ public final class ActivityThread { r.profileFile = profileName; r.profileFd = profileFd; r.autoStopProfiler = autoStopProfiler; + r.activityOptions = resumeArgs; updatePendingConfiguration(curConfig); @@ -1244,7 +1252,7 @@ public final class ActivityThread { switch (msg.what) { case LAUNCH_ACTIVITY: { Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart"); - ActivityClientRecord r = (ActivityClientRecord)msg.obj; + final ActivityClientRecord r = (ActivityClientRecord) msg.obj; r.packageInfo = getPackageInfoNoCheck( r.activityInfo.applicationInfo, r.compatInfo); @@ -1290,7 +1298,8 @@ public final class ActivityThread { break; case RESUME_ACTIVITY: Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityResume"); - handleResumeActivity((IBinder)msg.obj, true, + final Pair<IBinder, Bundle> resumeArgs = (Pair<IBinder, Bundle>) msg.obj; + handleResumeActivity(resumeArgs.first, resumeArgs.second, true, msg.arg1 != 0, true); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); break; @@ -1583,10 +1592,10 @@ public final class ActivityThread { /** * Creates the top level resources for the given package. */ - Resources getTopLevelResources(String resDir, String[] overlayDirs, + Resources getTopLevelResources(String resDir, String[] overlayDirs, String[] libDirs, int displayId, Configuration overrideConfiguration, LoadedApk pkgInfo) { - return mResourcesManager.getTopLevelResources(resDir, overlayDirs, displayId, + return mResourcesManager.getTopLevelResources(resDir, overlayDirs, libDirs, displayId, overrideConfiguration, pkgInfo.getCompatibilityInfo(), null); } @@ -2076,7 +2085,7 @@ public final class ActivityThread { + ", comp=" + name + ", token=" + token); } - return performLaunchActivity(r, null); + return performLaunchActivity(r, null, null); } public final Activity getActivity(IBinder token) { @@ -2129,7 +2138,8 @@ public final class ActivityThread { sendMessage(H.CLEAN_UP_CONTEXT, cci); } - private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) { + private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent, + Bundle options) { // System.out.println("##### [" + System.currentTimeMillis() + "] ActivityThread.performLaunchActivity(" + r + ")"); ActivityInfo aInfo = r.activityInfo; @@ -2187,7 +2197,7 @@ public final class ActivityThread { + r.activityInfo.name + " with config " + config); activity.attach(appContext, this, getInstrumentation(), r.token, r.ident, app, r.intent, r.activityInfo, title, r.parent, - r.embeddedID, r.lastNonConfigurationInstances, config); + r.embeddedID, r.lastNonConfigurationInstances, config, options); if (customIntent != null) { activity.mIntent = customIntent; @@ -2297,12 +2307,13 @@ public final class ActivityThread { if (localLOGV) Slog.v( TAG, "Handling launch of " + r); - Activity a = performLaunchActivity(r, customIntent); + + Activity a = performLaunchActivity(r, customIntent, r.activityOptions); if (a != null) { r.createdConfig = new Configuration(mConfiguration); Bundle oldState = r.state; - handleResumeActivity(r.token, false, r.isForward, + handleResumeActivity(r.token, r.activityOptions, false, r.isForward, !r.activity.mFinished && !r.startsNotResumed); if (!r.activity.mFinished && r.startsNotResumed) { @@ -2861,12 +2872,13 @@ public final class ActivityThread { r.mPendingRemoveWindowManager = null; } - final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, - boolean reallyResume) { + final void handleResumeActivity(IBinder token, Bundle resumeArgs, + boolean clearHide, boolean isForward, boolean reallyResume) { // If we are getting ready to gc after going to the background, well // we are back active so skip it. unscheduleGcIdler(); + // TODO Push resumeArgs into the activity for consideration ActivityClientRecord r = performResumeActivity(token, clearHide); if (r != null) { @@ -2991,11 +3003,17 @@ public final class ActivityThread { int h; if (w < 0) { Resources res = r.activity.getResources(); - mThumbnailHeight = h = - res.getDimensionPixelSize(com.android.internal.R.dimen.thumbnail_height); - - mThumbnailWidth = w = - res.getDimensionPixelSize(com.android.internal.R.dimen.thumbnail_width); + if (SystemProperties.getBoolean("persist.recents.use_alternate", false)) { + int wId = com.android.internal.R.dimen.recents_thumbnail_width; + int hId = com.android.internal.R.dimen.recents_thumbnail_height; + mThumbnailWidth = w = res.getDimensionPixelSize(wId); + mThumbnailHeight = h = res.getDimensionPixelSize(hId); + } else { + mThumbnailHeight = h = + res.getDimensionPixelSize(com.android.internal.R.dimen.thumbnail_height); + mThumbnailWidth = w = + res.getDimensionPixelSize(com.android.internal.R.dimen.thumbnail_width); + } } else { h = mThumbnailHeight; } @@ -3787,6 +3805,7 @@ public final class ActivityThread { } } r.startsNotResumed = tmp.startsNotResumed; + r.activityOptions = null; handleLaunchActivity(r, currentIntent); } @@ -3964,6 +3983,7 @@ public final class ActivityThread { ArrayList<ComponentCallbacks2> callbacks = collectComponentCallbacks(false, config); // Cleanup hardware accelerated stuff + // TODO: Do we actually want to do this in response to all config changes? WindowManagerGlobal.getInstance().trimLocalMemory(); freeTextLayoutCachesIfNeeded(configDiff); diff --git a/core/java/android/app/AlertDialog.java b/core/java/android/app/AlertDialog.java index 10d5e25..ab148a9 100644 --- a/core/java/android/app/AlertDialog.java +++ b/core/java/android/app/AlertDialog.java @@ -27,7 +27,6 @@ import android.os.Message; import android.util.TypedValue; import android.view.ContextThemeWrapper; import android.view.KeyEvent; -import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; import android.widget.AdapterView; @@ -147,10 +146,10 @@ public class AlertDialog extends Dialog implements DialogInterface { } /** - * Gets one of the buttons used in the dialog. - * <p> - * If a button does not exist in the dialog, null will be returned. - * + * Gets one of the buttons used in the dialog. Returns null if the specified + * button does not exist or the dialog has not yet been fully created (for + * example, via {@link #show()} or {@link #create()}). + * * @param whichButton The identifier of the button that should be returned. * For example, this can be * {@link DialogInterface#BUTTON_POSITIVE}. @@ -159,7 +158,7 @@ public class AlertDialog extends Dialog implements DialogInterface { public Button getButton(int whichButton) { return mAlert.getButton(whichButton); } - + /** * Gets the list view used in the dialog. * @@ -853,6 +852,21 @@ public class AlertDialog extends Dialog implements DialogInterface { } /** + * Set a custom view resource to be the contents of the Dialog. The + * resource will be inflated, adding all top-level views to the screen. + * + * @param layoutResId Resource ID to be inflated. + * @return This Builder object to allow for chaining of calls to set + * methods + */ + public Builder setView(int layoutResId) { + P.mView = null; + P.mViewLayoutResId = layoutResId; + P.mViewSpacingSpecified = false; + return this; + } + + /** * Set a custom view to be the contents of the Dialog. If the supplied view is an instance * of a {@link ListView} the light background will be used. * @@ -862,6 +876,7 @@ public class AlertDialog extends Dialog implements DialogInterface { */ public Builder setView(View view) { P.mView = view; + P.mViewLayoutResId = 0; P.mViewSpacingSpecified = false; return this; } @@ -891,6 +906,7 @@ public class AlertDialog extends Dialog implements DialogInterface { public Builder setView(View view, int viewSpacingLeft, int viewSpacingTop, int viewSpacingRight, int viewSpacingBottom) { P.mView = view; + P.mViewLayoutResId = 0; P.mViewSpacingSpecified = true; P.mViewSpacingLeft = viewSpacingLeft; P.mViewSpacingTop = viewSpacingTop; diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 079cf7a..b616c1e 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -780,6 +780,25 @@ public class AppOpsManager { } } + /** + * Set a non-persisted restriction on an audio operation at a stream-level. + * Restrictions are temporary additional constraints imposed on top of the persisted rules + * defined by {@link #setMode}. + * + * @param code The operation to restrict. + * @param stream The {@link android.media.AudioManager} stream type. + * @param mode The restriction mode (MODE_IGNORED,MODE_ERRORED) or MODE_ALLOWED to unrestrict. + * @param exceptionPackages Optional list of packages to exclude from the restriction. + * @hide + */ + public void setRestriction(int code, int stream, int mode, String[] exceptionPackages) { + try { + final int uid = Binder.getCallingUid(); + mService.setAudioRestriction(code, stream, uid, mode, exceptionPackages); + } catch (RemoteException e) { + } + } + /** @hide */ public void resetAllModes() { try { @@ -1009,6 +1028,35 @@ public class AppOpsManager { } /** + * Like {@link #checkOp} but at a stream-level for audio operations. + * @hide + */ + public int checkAudioOp(int op, int stream, int uid, String packageName) { + try { + final int mode = mService.checkAudioOperation(op, stream, uid, packageName); + if (mode == MODE_ERRORED) { + throw new SecurityException(buildSecurityExceptionMsg(op, uid, packageName)); + } + return mode; + } catch (RemoteException e) { + } + return MODE_IGNORED; + } + + /** + * Like {@link #checkAudioOp} but instead of throwing a {@link SecurityException} it + * returns {@link #MODE_ERRORED}. + * @hide + */ + public int checkAudioOpNoThrow(int op, int stream, int uid, String packageName) { + try { + return mService.checkAudioOperation(op, stream, uid, packageName); + } catch (RemoteException e) { + } + return MODE_IGNORED; + } + + /** * Make note of an application performing an operation. Note that you must pass * in both the uid and name of the application to be checked; this function will verify * that these two match, and if not, return {@link #MODE_IGNORED}. If this call diff --git a/core/java/android/app/ApplicationErrorReport.java b/core/java/android/app/ApplicationErrorReport.java index c117486..8b132e0 100644 --- a/core/java/android/app/ApplicationErrorReport.java +++ b/core/java/android/app/ApplicationErrorReport.java @@ -24,7 +24,6 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.Parcel; import android.os.Parcelable; -import android.os.SystemClock; import android.os.SystemProperties; import android.provider.Settings; import android.util.Printer; diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index c5e6ac4..ab62427 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -29,6 +29,7 @@ import android.content.pm.FeatureInfo; import android.content.pm.IPackageDataObserver; import android.content.pm.IPackageDeleteObserver; import android.content.pm.IPackageInstallObserver; +import android.content.pm.IPackageInstallObserver2; import android.content.pm.IPackageManager; import android.content.pm.IPackageMoveObserver; import android.content.pm.IPackageStatsObserver; @@ -57,8 +58,6 @@ import android.view.Display; import java.lang.ref.WeakReference; import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; import java.util.List; /*package*/ @@ -825,7 +824,7 @@ final class ApplicationPackageManager extends PackageManager { } Resources r = mContext.mMainThread.getTopLevelResources( app.uid == Process.myUid() ? app.sourceDir : app.publicSourceDir, - app.resourceDirs, Display.DEFAULT_DISPLAY, null, mContext.mPackageInfo); + app.resourceDirs, null, Display.DEFAULT_DISPLAY, null, mContext.mPackageInfo); if (r != null) { return r; } @@ -1093,7 +1092,7 @@ final class ApplicationPackageManager extends PackageManager { public void installPackage(Uri packageURI, IPackageInstallObserver observer, int flags, String installerPackageName) { try { - mPM.installPackage(packageURI, observer, flags, installerPackageName); + mPM.installPackageEtc(packageURI, observer, null, flags, installerPackageName); } catch (RemoteException e) { // Should never happen! } @@ -1104,20 +1103,58 @@ final class ApplicationPackageManager extends PackageManager { int flags, String installerPackageName, Uri verificationURI, ManifestDigest manifestDigest, ContainerEncryptionParams encryptionParams) { try { - mPM.installPackageWithVerification(packageURI, observer, flags, installerPackageName, - verificationURI, manifestDigest, encryptionParams); + mPM.installPackageWithVerificationEtc(packageURI, observer, null, flags, + installerPackageName, verificationURI, manifestDigest, encryptionParams); } catch (RemoteException e) { // Should never happen! } } @Override - public void installPackageWithVerificationAndEncryption(Uri packageURI, + public void installPackageWithVerificationAndEncryption(Uri packageURI, IPackageInstallObserver observer, int flags, String installerPackageName, VerificationParams verificationParams, ContainerEncryptionParams encryptionParams) { try { - mPM.installPackageWithVerificationAndEncryption(packageURI, observer, flags, - installerPackageName, verificationParams, encryptionParams); + mPM.installPackageWithVerificationAndEncryptionEtc(packageURI, observer, null, + flags, installerPackageName, verificationParams, encryptionParams); + } catch (RemoteException e) { + // Should never happen! + } + } + + // Expanded observer-API versions + @Override + public void installPackage(Uri packageURI, PackageInstallObserver observer, + int flags, String installerPackageName) { + try { + mPM.installPackageEtc(packageURI, null, observer.mObserver, + flags, installerPackageName); + } catch (RemoteException e) { + // Should never happen! + } + } + + @Override + public void installPackageWithVerification(Uri packageURI, + PackageInstallObserver observer, int flags, String installerPackageName, + Uri verificationURI, ManifestDigest manifestDigest, + ContainerEncryptionParams encryptionParams) { + try { + mPM.installPackageWithVerificationEtc(packageURI, null, observer.mObserver, flags, + installerPackageName, verificationURI, manifestDigest, encryptionParams); + } catch (RemoteException e) { + // Should never happen! + } + } + + @Override + public void installPackageWithVerificationAndEncryption(Uri packageURI, + PackageInstallObserver observer, int flags, String installerPackageName, + VerificationParams verificationParams, ContainerEncryptionParams encryptionParams) { + try { + mPM.installPackageWithVerificationAndEncryptionEtc(packageURI, null, + observer.mObserver, flags, installerPackageName, verificationParams, + encryptionParams); } catch (RemoteException e) { // Should never happen! } diff --git a/core/java/android/app/ApplicationThreadNative.java b/core/java/android/app/ApplicationThreadNative.java index cb453e2..f1c632e 100644 --- a/core/java/android/app/ApplicationThreadNative.java +++ b/core/java/android/app/ApplicationThreadNative.java @@ -113,7 +113,8 @@ public abstract class ApplicationThreadNative extends Binder IBinder b = data.readStrongBinder(); int procState = data.readInt(); boolean isForward = data.readInt() != 0; - scheduleResumeActivity(b, procState, isForward); + Bundle resumeArgs = data.readBundle(); + scheduleResumeActivity(b, procState, isForward, resumeArgs); return true; } @@ -145,8 +146,10 @@ public abstract class ApplicationThreadNative extends Binder ParcelFileDescriptor profileFd = data.readInt() != 0 ? ParcelFileDescriptor.CREATOR.createFromParcel(data) : null; boolean autoStopProfiler = data.readInt() != 0; + Bundle resumeArgs = data.readBundle(); scheduleLaunchActivity(intent, b, ident, info, curConfig, compatInfo, procState, state, - ri, pi, notResumed, isForward, profileName, profileFd, autoStopProfiler); + ri, pi, notResumed, isForward, profileName, profileFd, autoStopProfiler, + resumeArgs); return true; } @@ -705,20 +708,22 @@ class ApplicationThreadProxy implements IApplicationThread { data.recycle(); } - public final void scheduleResumeActivity(IBinder token, int procState, boolean isForward) + public final void scheduleResumeActivity(IBinder token, int procState, boolean isForward, + Bundle resumeArgs) throws RemoteException { Parcel data = Parcel.obtain(); data.writeInterfaceToken(IApplicationThread.descriptor); data.writeStrongBinder(token); data.writeInt(procState); data.writeInt(isForward ? 1 : 0); + data.writeBundle(resumeArgs); mRemote.transact(SCHEDULE_RESUME_ACTIVITY_TRANSACTION, data, null, IBinder.FLAG_ONEWAY); data.recycle(); } public final void scheduleSendResult(IBinder token, List<ResultInfo> results) - throws RemoteException { + throws RemoteException { Parcel data = Parcel.obtain(); data.writeInterfaceToken(IApplicationThread.descriptor); data.writeStrongBinder(token); @@ -731,9 +736,10 @@ class ApplicationThreadProxy implements IApplicationThread { public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident, ActivityInfo info, Configuration curConfig, CompatibilityInfo compatInfo, int procState, Bundle state, List<ResultInfo> pendingResults, - List<Intent> pendingNewIntents, boolean notResumed, boolean isForward, - String profileName, ParcelFileDescriptor profileFd, boolean autoStopProfiler) - throws RemoteException { + List<Intent> pendingNewIntents, boolean notResumed, boolean isForward, + String profileName, ParcelFileDescriptor profileFd, boolean autoStopProfiler, + Bundle resumeArgs) + throws RemoteException { Parcel data = Parcel.obtain(); data.writeInterfaceToken(IApplicationThread.descriptor); intent.writeToParcel(data, 0); @@ -756,6 +762,7 @@ class ApplicationThreadProxy implements IApplicationThread { data.writeInt(0); } data.writeInt(autoStopProfiler ? 1 : 0); + data.writeBundle(resumeArgs); mRemote.transact(SCHEDULE_LAUNCH_ACTIVITY_TRANSACTION, data, null, IBinder.FLAG_ONEWAY); data.recycle(); @@ -886,7 +893,7 @@ class ApplicationThreadProxy implements IApplicationThread { } public final void scheduleServiceArgs(IBinder token, boolean taskRemoved, int startId, - int flags, Intent args) throws RemoteException { + int flags, Intent args) throws RemoteException { Parcel data = Parcel.obtain(); data.writeInterfaceToken(IApplicationThread.descriptor); data.writeStrongBinder(token); diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 924d656..77b5485 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -17,6 +17,7 @@ package android.app; import android.os.Build; + import com.android.internal.policy.PolicyManager; import com.android.internal.util.Preconditions; @@ -35,7 +36,9 @@ import android.content.ReceiverCallNotAllowedException; import android.content.ServiceConnection; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; +import android.content.pm.ILauncherApps; import android.content.pm.IPackageManager; +import android.content.pm.LauncherApps; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.AssetManager; @@ -64,18 +67,23 @@ import android.location.ILocationManager; import android.location.LocationManager; import android.media.AudioManager; import android.media.MediaRouter; +import android.media.session.MediaSessionManager; import android.net.ConnectivityManager; import android.net.IConnectivityManager; import android.net.INetworkPolicyManager; import android.net.NetworkPolicyManager; +import android.net.NetworkScoreManager; import android.net.Uri; import android.net.nsd.INsdManager; import android.net.nsd.NsdManager; import android.net.wifi.IWifiManager; import android.net.wifi.WifiManager; +import android.net.wifi.hotspot.IWifiHotspotManager; +import android.net.wifi.hotspot.WifiHotspotManager; import android.net.wifi.p2p.IWifiP2pManager; import android.net.wifi.p2p.WifiP2pManager; import android.nfc.NfcManager; +import android.os.BatteryManager; import android.os.Binder; import android.os.Bundle; import android.os.Debug; @@ -99,6 +107,8 @@ import android.os.storage.StorageManager; import android.print.IPrintManager; import android.print.PrintManager; import android.telephony.TelephonyManager; +import android.tv.ITvInputManager; +import android.tv.TvInputManager; import android.content.ClipboardManager; import android.util.AndroidRuntimeException; import android.util.ArrayMap; @@ -115,6 +125,7 @@ import android.view.textservice.TextServicesManager; import android.accounts.AccountManager; import android.accounts.IAccountManager; import android.app.admin.DevicePolicyManager; +import android.app.trust.TrustManager; import com.android.internal.annotations.GuardedBy; import com.android.internal.app.IAppOpsService; @@ -395,6 +406,11 @@ class ContextImpl extends Context { return new DownloadManager(ctx.getContentResolver(), ctx.getPackageName()); }}); + registerService(BATTERY_SERVICE, new ServiceFetcher() { + public Object createService(ContextImpl ctx) { + return new BatteryManager(); + }}); + registerService(NFC_SERVICE, new ServiceFetcher() { public Object createService(ContextImpl ctx) { return new NfcManager(ctx); @@ -463,7 +479,8 @@ class ContextImpl extends Context { outerContext.getApplicationInfo().targetSdkVersion, com.android.internal.R.style.Theme_Dialog, com.android.internal.R.style.Theme_Holo_Dialog, - com.android.internal.R.style.Theme_DeviceDefault_Dialog)), + com.android.internal.R.style.Theme_DeviceDefault_Dialog, + com.android.internal.R.style.Theme_DeviceDefault_Light_Dialog)), ctx.mMainThread.getHandler()); }}); @@ -550,6 +567,13 @@ class ContextImpl extends Context { return new WifiManager(ctx.getOuterContext(), service); }}); + registerService(WIFI_HOTSPOT_SERVICE, new ServiceFetcher() { + public Object createService(ContextImpl ctx) { + IBinder b = ServiceManager.getService(WIFI_HOTSPOT_SERVICE); + IWifiHotspotManager service = IWifiHotspotManager.Stub.asInterface(b); + return new WifiHotspotManager(ctx.getOuterContext(), service); + }}); + registerService(WIFI_P2P_SERVICE, new ServiceFetcher() { public Object createService(ContextImpl ctx) { IBinder b = ServiceManager.getService(WIFI_P2P_SERVICE); @@ -592,6 +616,14 @@ class ContextImpl extends Context { } }); + registerService(LAUNCHER_APPS_SERVICE, new ServiceFetcher() { + public Object createService(ContextImpl ctx) { + IBinder b = ServiceManager.getService(LAUNCHER_APPS_SERVICE); + ILauncherApps service = ILauncherApps.Stub.asInterface(b); + return new LauncherApps(ctx, service); + } + }); + registerService(PRINT_SERVICE, new ServiceFetcher() { public Object createService(ContextImpl ctx) { IBinder iBinder = ServiceManager.getService(Context.PRINT_SERVICE); @@ -604,6 +636,31 @@ class ContextImpl extends Context { public Object createService(ContextImpl ctx) { return new ConsumerIrManager(ctx); }}); + + registerService(MEDIA_SESSION_SERVICE, new ServiceFetcher() { + public Object createService(ContextImpl ctx) { + return new MediaSessionManager(ctx); + } + }); + registerService(TRUST_SERVICE, new ServiceFetcher() { + public Object createService(ContextImpl ctx) { + IBinder b = ServiceManager.getService(TRUST_SERVICE); + return new TrustManager(b); + } + }); + + registerService(TV_INPUT_SERVICE, new ServiceFetcher() { + public Object createService(ContextImpl ctx) { + IBinder iBinder = ServiceManager.getService(TV_INPUT_SERVICE); + ITvInputManager service = ITvInputManager.Stub.asInterface(iBinder); + return new TvInputManager(service, UserHandle.myUserId()); + }}); + + registerService(NETWORK_SCORE_SERVICE, new ServiceFetcher() { + public Object createService(ContextImpl ctx) { + return new NetworkScoreManager(ctx); + } + }); } static ContextImpl getImpl(Context context) { @@ -1480,13 +1537,13 @@ class ContextImpl extends Context { private void validateServiceIntent(Intent service) { if (service.getComponent() == null && service.getPackage() == null) { - if (true || getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.KITKAT) { + if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.L) { + IllegalArgumentException ex = new IllegalArgumentException( + "Service Intent must be explicit: " + service); + throw ex; + } else { Log.w(TAG, "Implicit intents with startService are not safe: " + service + " " + Debug.getCallers(2, 3)); - //IllegalArgumentException ex = new IllegalArgumentException( - // "Service Intent must be explicit: " + service); - //Log.e(TAG, "This will become an error", ex); - //throw ex; } } } @@ -2036,8 +2093,9 @@ class ContextImpl extends Context { || (compatInfo != null && compatInfo.applicationScale != resources.getCompatibilityInfo().applicationScale)) { resources = mResourcesManager.getTopLevelResources( - packageInfo.getResDir(), packageInfo.getOverlayDirs(), displayId, - overrideConfiguration, compatInfo, activityToken); + packageInfo.getResDir(), packageInfo.getOverlayDirs(), + packageInfo.getApplicationInfo().sharedLibraryFiles, + displayId, overrideConfiguration, compatInfo, activityToken); } } mResources = resources; diff --git a/core/java/android/app/Dialog.java b/core/java/android/app/Dialog.java index a8277b5..07583fd 100644 --- a/core/java/android/app/Dialog.java +++ b/core/java/android/app/Dialog.java @@ -17,8 +17,7 @@ package android.app; import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import com.android.internal.app.ActionBarImpl; +import com.android.internal.app.WindowDecorActionBar; import com.android.internal.policy.PolicyManager; import android.content.ComponentName; @@ -88,7 +87,7 @@ public class Dialog implements DialogInterface, Window.Callback, final WindowManager mWindowManager; Window mWindow; View mDecor; - private ActionBarImpl mActionBar; + private ActionBar mActionBar; /** * This field should be made private, so it is hidden from the SDK. * {@hide} @@ -240,6 +239,18 @@ public class Dialog implements DialogInterface, Window.Callback, } /** + * Forces immediate creation of the dialog. + * <p> + * Note that you should not override this method to perform dialog creation. + * Rather, override {@link #onCreate(Bundle)}. + */ + public void create() { + if (!mCreated) { + dispatchOnCreate(null); + } + } + + /** * Start the dialog and display it on screen. The window is placed in the * application layer and opaque. Note that you should not override this * method to do initialization when the dialog is shown, instead implement @@ -269,7 +280,7 @@ public class Dialog implements DialogInterface, Window.Callback, final ApplicationInfo info = mContext.getApplicationInfo(); mWindow.setDefaultIcon(info.icon); mWindow.setDefaultLogo(info.logo); - mActionBar = new ActionBarImpl(this); + mActionBar = new WindowDecorActionBar(this); } WindowManager.LayoutParams l = mWindow.getAttributes(); @@ -457,11 +468,12 @@ public class Dialog implements DialogInterface, Window.Callback, } /** - * Finds a view that was identified by the id attribute from the XML that - * was processed in {@link #onStart}. + * Finds a child view with the given identifier. Returns null if the + * specified child view does not exist or the dialog has not yet been fully + * created (for example, via {@link #show()} or {@link #create()}). * * @param id the identifier of the view to find - * @return The view if found or null otherwise. + * @return The view with the given id or null. */ public View findViewById(int id) { return mWindow.findViewById(id); @@ -480,7 +492,7 @@ public class Dialog implements DialogInterface, Window.Callback, /** * Set the screen content to an explicit view. This view is placed * directly into the screen's view hierarchy. It can itself be a complex - * view hierarhcy. + * view hierarchy. * * @param view The desired content to display. */ diff --git a/core/java/android/app/ExpandableListActivity.java b/core/java/android/app/ExpandableListActivity.java index 9651078..e08f25a 100644 --- a/core/java/android/app/ExpandableListActivity.java +++ b/core/java/android/app/ExpandableListActivity.java @@ -27,7 +27,6 @@ import android.widget.ExpandableListAdapter; import android.widget.ExpandableListView; import android.widget.SimpleCursorTreeAdapter; import android.widget.SimpleExpandableListAdapter; -import android.widget.AdapterView.AdapterContextMenuInfo; import java.util.Map; diff --git a/core/java/android/app/Fragment.java b/core/java/android/app/Fragment.java index af8f177..6c0d379 100644 --- a/core/java/android/app/Fragment.java +++ b/core/java/android/app/Fragment.java @@ -17,6 +17,7 @@ package android.app; import android.animation.Animator; +import android.annotation.Nullable; import android.content.ComponentCallbacks2; import android.content.Context; import android.content.Intent; @@ -575,7 +576,7 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene * the given fragment class. This is a runtime exception; it is not * normally expected to happen. */ - public static Fragment instantiate(Context context, String fname, Bundle args) { + public static Fragment instantiate(Context context, String fname, @Nullable Bundle args) { try { Class<?> clazz = sClassMap.get(fname); if (clazz == null) { @@ -1213,7 +1214,8 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene * * @return Return the View for the fragment's UI, or null. */ - public View onCreateView(LayoutInflater inflater, ViewGroup container, + @Nullable + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { return null; } @@ -1228,7 +1230,7 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene * @param savedInstanceState If non-null, this fragment is being re-constructed * from a previous saved state as given here. */ - public void onViewCreated(View view, Bundle savedInstanceState) { + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { } /** @@ -1237,6 +1239,7 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene * * @return The fragment's root view, or null if it has no layout. */ + @Nullable public View getView() { return mView; } diff --git a/core/java/android/app/FragmentBreadCrumbs.java b/core/java/android/app/FragmentBreadCrumbs.java index b810b89..e4de7af 100644 --- a/core/java/android/app/FragmentBreadCrumbs.java +++ b/core/java/android/app/FragmentBreadCrumbs.java @@ -81,14 +81,19 @@ public class FragmentBreadCrumbs extends ViewGroup } public FragmentBreadCrumbs(Context context, AttributeSet attrs) { - this(context, attrs, android.R.style.Widget_FragmentBreadCrumbs); + this(context, attrs, com.android.internal.R.attr.fragmentBreadCrumbsStyle); } - public FragmentBreadCrumbs(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public FragmentBreadCrumbs(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public FragmentBreadCrumbs( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.FragmentBreadCrumbs, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.FragmentBreadCrumbs, defStyleAttr, defStyleRes); mGravity = a.getInt(com.android.internal.R.styleable.FragmentBreadCrumbs_gravity, DEFAULT_GRAVITY); diff --git a/core/java/android/app/IActivityManager.java b/core/java/android/app/IActivityManager.java index 4e2a57d..bfbd339 100644 --- a/core/java/android/app/IActivityManager.java +++ b/core/java/android/app/IActivityManager.java @@ -122,6 +122,7 @@ public interface IActivityManager extends IInterface { public void resizeStack(int stackId, Rect bounds) throws RemoteException; public List<StackInfo> getAllStackInfos() throws RemoteException; public StackInfo getStackInfo(int stackId) throws RemoteException; + public boolean isInHomeStack(int taskId) throws RemoteException; public void setFocusedStack(int stackId) throws RemoteException; public int getTaskForActivity(IBinder token, boolean onlyRoot) throws RemoteException; /* oneway */ @@ -241,7 +242,8 @@ public interface IActivityManager extends IInterface { public void enterSafeMode() throws RemoteException; - public void noteWakeupAlarm(IIntentSender sender) throws RemoteException; + public void noteWakeupAlarm(IIntentSender sender, int sourceUid, String sourcePkg) + throws RemoteException; public boolean killPids(int[] pids, String reason, boolean secure) throws RemoteException; public boolean killProcessesBelowForeground(String reason) throws RemoteException; @@ -344,6 +346,7 @@ public interface IActivityManager extends IInterface { // Multi-user APIs public boolean switchUser(int userid) throws RemoteException; + public boolean startUserInBackground(int userid) throws RemoteException; public int stopUser(int userid, IStopUserCallback callback) throws RemoteException; public UserInfo getCurrentUser() throws RemoteException; public boolean isUserRunning(int userid, boolean orStopping) throws RemoteException; @@ -362,6 +365,8 @@ public interface IActivityManager extends IInterface { public Intent getIntentForIntentSender(IIntentSender sender) throws RemoteException; + public String getTagForIntentSender(IIntentSender sender, String prefix) throws RemoteException; + public void updatePersistentConfiguration(Configuration values) throws RemoteException; public long[] getProcessPss(int[] pids) throws RemoteException; @@ -415,6 +420,22 @@ public interface IActivityManager extends IInterface { public IBinder getHomeActivityToken() throws RemoteException; + /** @hide */ + public void startLockTaskMode(int taskId) throws RemoteException; + + /** @hide */ + public void startLockTaskMode(IBinder token) throws RemoteException; + + /** @hide */ + public void stopLockTaskMode() throws RemoteException; + + /** @hide */ + public boolean isInLockTaskMode() throws RemoteException; + + /** @hide */ + public void setActivityLabelAndIcon(IBinder token, CharSequence activityLabel, + Bitmap activityBitmap) throws RemoteException; + /* * Private non-Binder interfaces */ @@ -701,4 +722,15 @@ public interface IActivityManager extends IInterface { int GET_HOME_ACTIVITY_TOKEN_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+183; int GET_ACTIVITY_CONTAINER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+184; int DELETE_ACTIVITY_CONTAINER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+185; + + + // Start of L transactions + int GET_TAG_FOR_INTENT_SENDER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+210; + int START_USER_IN_BACKGROUND_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+211; + int IS_IN_HOME_STACK_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+212; + int START_LOCK_TASK_BY_TASK_ID_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+213; + int START_LOCK_TASK_BY_TOKEN_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+214; + int STOP_LOCK_TASK_MODE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+215; + int IS_IN_LOCK_TASK_MODE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+216; + int SET_ACTIVITY_LABEL_ICON_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+217; } diff --git a/core/java/android/app/IApplicationThread.java b/core/java/android/app/IApplicationThread.java index 3aceff9..ac8ac8f 100644 --- a/core/java/android/app/IApplicationThread.java +++ b/core/java/android/app/IApplicationThread.java @@ -50,15 +50,16 @@ public interface IApplicationThread extends IInterface { int configChanges) throws RemoteException; void scheduleWindowVisibility(IBinder token, boolean showWindow) throws RemoteException; void scheduleSleeping(IBinder token, boolean sleeping) throws RemoteException; - void scheduleResumeActivity(IBinder token, int procState, boolean isForward) + void scheduleResumeActivity(IBinder token, int procState, boolean isForward, Bundle resumeArgs) throws RemoteException; void scheduleSendResult(IBinder token, List<ResultInfo> results) throws RemoteException; void scheduleLaunchActivity(Intent intent, IBinder token, int ident, ActivityInfo info, Configuration curConfig, CompatibilityInfo compatInfo, int procState, Bundle state, List<ResultInfo> pendingResults, - List<Intent> pendingNewIntents, boolean notResumed, boolean isForward, - String profileName, ParcelFileDescriptor profileFd, boolean autoStopProfiler) - throws RemoteException; + List<Intent> pendingNewIntents, boolean notResumed, boolean isForward, + String profileName, ParcelFileDescriptor profileFd, boolean autoStopProfiler, + Bundle resumeArgs) + throws RemoteException; void scheduleRelaunchActivity(IBinder token, List<ResultInfo> pendingResults, List<Intent> pendingNewIntents, int configChanges, boolean notResumed, Configuration config) throws RemoteException; diff --git a/core/java/android/app/IBackupAgent.aidl b/core/java/android/app/IBackupAgent.aidl index 087f83c..7036aea 100644 --- a/core/java/android/app/IBackupAgent.aidl +++ b/core/java/android/app/IBackupAgent.aidl @@ -76,8 +76,9 @@ oneway interface IBackupAgent { * @param callbackBinder Binder on which to indicate operation completion, * passed here as a convenience to the agent. */ - void doRestore(in ParcelFileDescriptor data, int appVersionCode, - in ParcelFileDescriptor newState, int token, IBackupManager callbackBinder); + void doRestore(in ParcelFileDescriptor data, + int appVersionCode, in ParcelFileDescriptor newState, + int token, IBackupManager callbackBinder); /** * Perform a "full" backup to the given file descriptor. The output file is presumed @@ -112,8 +113,23 @@ oneway interface IBackupAgent { * @param path Relative path of the file within its semantic domain. * @param mode Access mode of the file system entity, e.g. 0660. * @param mtime Last modification time of the file system entity. + * @param token Opaque token identifying this transaction. This must + * be echoed back to the backup service binder once the agent is + * finished restoring the application based on the restore data + * contents. + * @param callbackBinder Binder on which to indicate operation completion, + * passed here as a convenience to the agent. */ void doRestoreFile(in ParcelFileDescriptor data, long size, int type, String domain, String path, long mode, long mtime, int token, IBackupManager callbackBinder); + + /** + * Out of band: instruct the agent to crash within the client process. This is used + * when the backup infrastructure detects a semantic error post-hoc and needs to + * pass the problem back to the app. + * + * @param message The message to be passed to the agent's application in an exception. + */ + void fail(String message); } diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index 9911467..bb6eeda 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -31,7 +31,7 @@ interface INotificationManager void enqueueToast(String pkg, ITransientNotification callback, int duration); void cancelToast(String pkg, ITransientNotification callback); - void enqueueNotificationWithTag(String pkg, String basePkg, String tag, int id, + void enqueueNotificationWithTag(String pkg, String opPkg, String tag, int id, in Notification notification, inout int[] idReceived, int userId); void cancelNotificationWithTag(String pkg, String tag, int id, int userId); diff --git a/core/java/android/app/IUiAutomationConnection.aidl b/core/java/android/app/IUiAutomationConnection.aidl index 09bf829..347de97 100644 --- a/core/java/android/app/IUiAutomationConnection.aidl +++ b/core/java/android/app/IUiAutomationConnection.aidl @@ -19,6 +19,8 @@ package android.app; import android.accessibilityservice.IAccessibilityServiceClient; import android.graphics.Bitmap; import android.view.InputEvent; +import android.view.WindowContentFrameStats; +import android.view.WindowAnimationFrameStats; import android.os.ParcelFileDescriptor; /** @@ -26,7 +28,7 @@ import android.os.ParcelFileDescriptor; * on behalf of an instrumentation that it runs. These operations require * special permissions which the shell user has but the instrumentation does * not. Running privileged operations by the shell user on behalf of an - * instrumentation is needed for running UiTestCases. + * instrumentation is needed for running UiTestCases. * * {@hide} */ @@ -37,4 +39,8 @@ interface IUiAutomationConnection { boolean setRotation(int rotation); Bitmap takeScreenshot(int width, int height); void shutdown(); + boolean clearWindowContentFrameStats(int windowId); + WindowContentFrameStats getWindowContentFrameStats(int windowId); + void clearWindowAnimationFrameStats(); + WindowAnimationFrameStats getWindowAnimationFrameStats(); } diff --git a/core/java/android/app/LoadedApk.java b/core/java/android/app/LoadedApk.java index d409352..3ae8bfc 100644 --- a/core/java/android/app/LoadedApk.java +++ b/core/java/android/app/LoadedApk.java @@ -41,6 +41,7 @@ import android.os.Trace; import android.os.UserHandle; import android.util.AndroidRuntimeException; import android.util.Slog; +import android.util.SparseArray; import android.view.DisplayAdjustments; import android.view.Display; @@ -48,6 +49,8 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.ref.WeakReference; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import java.net.URL; import java.util.Enumeration; @@ -485,7 +488,7 @@ public final class LoadedApk { public Resources getResources(ActivityThread mainThread) { if (mResources == null) { mResources = mainThread.getTopLevelResources(mResDir, mOverlayDirs, - Display.DEFAULT_DISPLAY, null, this); + mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, null, this); } return mResources; } @@ -530,10 +533,101 @@ public final class LoadedApk { } } } - + + // Rewrite the R 'constants' for all library apks. + SparseArray<String> packageIdentifiers = getAssets(mActivityThread) + .getAssignedPackageIdentifiers(); + final int N = packageIdentifiers.size(); + for (int i = 0; i < N; i++) { + final int id = packageIdentifiers.keyAt(i); + if (id == 0x01 || id == 0x7f) { + continue; + } + + rewriteRValues(getClassLoader(), packageIdentifiers.valueAt(i), id); + } + return app; } + private void rewriteIntField(Field field, int packageId) throws IllegalAccessException { + int requiredModifiers = Modifier.STATIC | Modifier.PUBLIC; + int bannedModifiers = Modifier.FINAL; + + int mod = field.getModifiers(); + if ((mod & requiredModifiers) != requiredModifiers || + (mod & bannedModifiers) != 0) { + throw new IllegalArgumentException("Field " + field.getName() + + " is not rewritable"); + } + + if (field.getType() != int.class && field.getType() != Integer.class) { + throw new IllegalArgumentException("Field " + field.getName() + + " is not an integer"); + } + + try { + int resId = field.getInt(null); + field.setInt(null, (resId & 0x00ffffff) | (packageId << 24)); + } catch (IllegalAccessException e) { + // This should not occur (we check above if we can write to it) + throw new IllegalArgumentException(e); + } + } + + private void rewriteIntArrayField(Field field, int packageId) { + int requiredModifiers = Modifier.STATIC | Modifier.PUBLIC; + + if ((field.getModifiers() & requiredModifiers) != requiredModifiers) { + throw new IllegalArgumentException("Field " + field.getName() + + " is not rewritable"); + } + + if (field.getType() != int[].class) { + throw new IllegalArgumentException("Field " + field.getName() + + " is not an integer array"); + } + + try { + int[] array = (int[]) field.get(null); + for (int i = 0; i < array.length; i++) { + array[i] = (array[i] & 0x00ffffff) | (packageId << 24); + } + } catch (IllegalAccessException e) { + // This should not occur (we check above if we can write to it) + throw new IllegalArgumentException(e); + } + } + + private void rewriteRValues(ClassLoader cl, String packageName, int id) { + try { + final Class<?> rClazz = cl.loadClass(packageName + ".R"); + Class<?>[] declaredClasses = rClazz.getDeclaredClasses(); + for (Class<?> clazz : declaredClasses) { + try { + if (clazz.getSimpleName().equals("styleable")) { + for (Field field : clazz.getDeclaredFields()) { + if (field.getType() == int[].class) { + rewriteIntArrayField(field, id); + } + } + + } else { + for (Field field : clazz.getDeclaredFields()) { + rewriteIntField(field, id); + } + } + } catch (Exception e) { + throw new IllegalArgumentException("Failed to rewrite R values for " + + clazz.getName(), e); + } + } + + } catch (Exception e) { + throw new IllegalArgumentException("Failed to rewrite R values", e); + } + } + public void removeContextRegistrations(Context context, String who, String what) { final boolean reportRegistrationLeaks = StrictMode.vmRegistrationLeaksEnabled(); diff --git a/core/java/android/app/MediaRouteButton.java b/core/java/android/app/MediaRouteButton.java index a7982f4..fa2813e 100644 --- a/core/java/android/app/MediaRouteButton.java +++ b/core/java/android/app/MediaRouteButton.java @@ -73,13 +73,18 @@ public class MediaRouteButton extends View { } public MediaRouteButton(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); + this(context, attrs, defStyleAttr, 0); + } + + public MediaRouteButton( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); mRouter = (MediaRouter)context.getSystemService(Context.MEDIA_ROUTER_SERVICE); mCallback = new MediaRouterCallback(); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.MediaRouteButton, defStyleAttr, 0); + final TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.MediaRouteButton, defStyleAttr, defStyleRes); setRemoteIndicatorDrawable(a.getDrawable( com.android.internal.R.styleable.MediaRouteButton_externalRouteEnabledDrawable)); mMinWidth = a.getDimensionPixelSize( diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index e6b5de1..fe629f6 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -17,11 +17,14 @@ package android.app; import com.android.internal.R; +import com.android.internal.util.LegacyNotificationUtil; +import android.annotation.IntDef; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.graphics.Bitmap; +import android.graphics.PorterDuff; import android.media.AudioManager; import android.net.Uri; import android.os.BadParcelableException; @@ -37,6 +40,8 @@ import android.view.View; import android.widget.ProgressBar; import android.widget.RemoteViews; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.text.NumberFormat; import java.util.ArrayList; @@ -201,6 +206,15 @@ public class Notification implements Parcelable */ public RemoteViews bigContentView; + + /** + * @hide + * A medium-format version of {@link #contentView}, giving the Notification an + * opportunity to add action buttons to contentView. The system UI may + * choose to show this as a popup notification at its discretion. + */ + public RemoteViews headsUpContentView; + /** * The bitmap that may escape the bounds of the panel and bar. */ @@ -357,6 +371,11 @@ public class Notification implements Parcelable public int flags; + /** @hide */ + @IntDef({PRIORITY_DEFAULT,PRIORITY_LOW,PRIORITY_MIN,PRIORITY_HIGH,PRIORITY_MAX}) + @Retention(RetentionPolicy.SOURCE) + public @interface Priority {} + /** * Default notification {@link #priority}. If your application does not prioritize its own * notifications, use this value for all notifications. @@ -398,8 +417,34 @@ public class Notification implements Parcelable * system will make a determination about how to interpret this priority when presenting * the notification. */ + @Priority public int priority; + + /** + * Sphere of visibility of this notification, which affects how and when the SystemUI reveals + * the notification's presence and contents in untrusted situations (namely, on the secure + * lockscreen). + * + * The default level, {@link #VISIBILITY_PRIVATE}, behaves exactly as notifications have always + * done on Android: The notification's {@link #icon} and {@link #tickerText} (if available) are + * shown in all situations, but the contents are only available if the device is unlocked for + * the appropriate user. + * + * A more permissive policy can be expressed by {@link #VISIBILITY_PUBLIC}; such a notification + * can be read even in an "insecure" context (that is, above a secure lockscreen). + * To modify the public version of this notification—for example, to redact some portions—see + * {@link Builder#setPublicVersion(Notification)}. + * + * Finally, a notification can be made {@link #VISIBILITY_SECRET}, which will suppress its icon + * and ticker until the user has bypassed the lockscreen. + */ + public int visibility; + + public static final int VISIBILITY_PUBLIC = 1; + public static final int VISIBILITY_PRIVATE = 0; + public static final int VISIBILITY_SECRET = -1; + /** * Notification category: incoming call (voice or video) or similar synchronous communication request. */ @@ -588,6 +633,7 @@ public class Notification implements Parcelable * notifications, each of which was supplied to {@link InboxStyle#addLine(CharSequence)}. */ public static final String EXTRA_TEXT_LINES = "android.textLines"; + public static final String EXTRA_TEMPLATE = "android.template"; /** * {@link #extras} key: An array of people that this notification relates to, specified @@ -716,6 +762,13 @@ public class Notification implements Parcelable public Action[] actions; /** + * Replacement version of this notification whose content will be shown + * in an insecure context such as atop a secure keyguard. See {@link #visibility} + * and {@link #VISIBILITY_PUBLIC}. + */ + public Notification publicVersion; + + /** * Constructs a Notification object with default values. * You might want to consider using {@link Builder} instead. */ @@ -814,6 +867,16 @@ public class Notification implements Parcelable if (parcel.readInt() != 0) { bigContentView = RemoteViews.CREATOR.createFromParcel(parcel); } + + if (parcel.readInt() != 0) { + headsUpContentView = RemoteViews.CREATOR.createFromParcel(parcel); + } + + visibility = parcel.readInt(); + + if (parcel.readInt() != 0) { + publicVersion = Notification.CREATOR.createFromParcel(parcel); + } } @Override @@ -894,6 +957,17 @@ public class Notification implements Parcelable that.bigContentView = this.bigContentView.clone(); } + if (heavy && this.headsUpContentView != null) { + that.headsUpContentView = this.headsUpContentView.clone(); + } + + that.visibility = this.visibility; + + if (this.publicVersion != null) { + that.publicVersion = new Notification(); + this.publicVersion.cloneInto(that.publicVersion, heavy); + } + if (!heavy) { that.lightenPayload(); // will clean out extras } @@ -908,6 +982,7 @@ public class Notification implements Parcelable tickerView = null; contentView = null; bigContentView = null; + headsUpContentView = null; largeIcon = null; if (extras != null) { extras.remove(Notification.EXTRA_LARGE_ICON); @@ -1019,6 +1094,22 @@ public class Notification implements Parcelable } else { parcel.writeInt(0); } + + if (headsUpContentView != null) { + parcel.writeInt(1); + headsUpContentView.writeToParcel(parcel, 0); + } else { + parcel.writeInt(0); + } + + parcel.writeInt(visibility); + + if (publicVersion != null) { + parcel.writeInt(1); + publicVersion.writeToParcel(parcel, 0); + } else { + parcel.writeInt(0); + } } /** @@ -1152,6 +1243,9 @@ public class Notification implements Parcelable if (bigContentView != null) { bigContentView.setUser(user); } + if (headsUpContentView != null) { + headsUpContentView.setUser(user); + } } /** @@ -1213,6 +1307,11 @@ public class Notification implements Parcelable private boolean mUseChronometer; private Style mStyle; private boolean mShowWhen = true; + private int mVisibility = VISIBILITY_PRIVATE; + private Notification mPublicVersion = null; + private boolean mQuantumTheme; + private final LegacyNotificationUtil mLegacyNotificationUtil; + private ArrayList<String> mPeople; /** * Constructs a new Builder with the defaults: @@ -1240,6 +1339,14 @@ public class Notification implements Parcelable mWhen = System.currentTimeMillis(); mAudioStreamType = STREAM_DEFAULT; mPriority = PRIORITY_DEFAULT; + mPeople = new ArrayList<String>(); + + // TODO: Decide on targetSdk from calling app whether to use quantum theme. + mQuantumTheme = true; + + // TODO: Decide on targetSdk from calling app whether to instantiate the processor at + // all. + mLegacyNotificationUtil = LegacyNotificationUtil.getInstance(); } /** @@ -1602,7 +1709,7 @@ public class Notification implements Parcelable * * @see Notification#priority */ - public Builder setPriority(int pri) { + public Builder setPriority(@Priority int pri) { mPriority = pri; return this; } @@ -1618,6 +1725,16 @@ public class Notification implements Parcelable } /** + * Add a person that is relevant to this notification. + * + * @see Notification#EXTRA_PEOPLE + */ + public Builder addPerson(String handle) { + mPeople.add(handle); + return this; + } + + /** * Merge additional metadata into this notification. * * <p>Values within the Bundle will replace existing extras values in this Builder. @@ -1704,6 +1821,30 @@ public class Notification implements Parcelable return this; } + /** + * Specify the value of {@link #visibility}. + + * @param visibility One of {@link #VISIBILITY_PRIVATE} (the default), + * {@link #VISIBILITY_SECRET}, or {@link #VISIBILITY_PUBLIC}. + * + * @return The same Builder. + */ + public Builder setVisibility(int visibility) { + mVisibility = visibility; + return this; + } + + /** + * Supply a replacement Notification whose contents should be shown in insecure contexts + * (i.e. atop the secure lockscreen). See {@link #visibility} and {@link #VISIBILITY_PUBLIC}. + * @param n A replacement notification, presumably with some or all info redacted. + * @return The same Builder. + */ + public Builder setPublicVersion(Notification n) { + mPublicVersion = n; + return this; + } + private void setFlag(int mask, boolean value) { if (value) { mFlags |= mask; @@ -1717,42 +1858,50 @@ public class Notification implements Parcelable boolean showLine3 = false; boolean showLine2 = false; int smallIconImageViewId = R.id.icon; - if (mLargeIcon != null) { - contentView.setImageViewBitmap(R.id.icon, mLargeIcon); - smallIconImageViewId = R.id.right_icon; - } - if (mPriority < PRIORITY_LOW) { + if (!mQuantumTheme && mPriority < PRIORITY_LOW) { contentView.setInt(R.id.icon, "setBackgroundResource", R.drawable.notification_template_icon_low_bg); contentView.setInt(R.id.status_bar_latest_event_content, "setBackgroundResource", R.drawable.notification_bg_low); } + if (mLargeIcon != null) { + contentView.setImageViewBitmap(R.id.icon, mLargeIcon); + processLegacyLargeIcon(mLargeIcon, contentView); + smallIconImageViewId = R.id.right_icon; + } if (mSmallIcon != 0) { contentView.setImageViewResource(smallIconImageViewId, mSmallIcon); contentView.setViewVisibility(smallIconImageViewId, View.VISIBLE); + if (mLargeIcon != null) { + processLegacySmallIcon(mSmallIcon, smallIconImageViewId, contentView); + } else { + processLegacyLargeIcon(mSmallIcon, contentView); + } + } else { contentView.setViewVisibility(smallIconImageViewId, View.GONE); } if (mContentTitle != null) { - contentView.setTextViewText(R.id.title, mContentTitle); + contentView.setTextViewText(R.id.title, processLegacyText(mContentTitle)); } if (mContentText != null) { - contentView.setTextViewText(R.id.text, mContentText); + contentView.setTextViewText(R.id.text, processLegacyText(mContentText)); showLine3 = true; } if (mContentInfo != null) { - contentView.setTextViewText(R.id.info, mContentInfo); + contentView.setTextViewText(R.id.info, processLegacyText(mContentInfo)); contentView.setViewVisibility(R.id.info, View.VISIBLE); showLine3 = true; } else if (mNumber > 0) { final int tooBig = mContext.getResources().getInteger( R.integer.status_bar_notification_info_maxnum); if (mNumber > tooBig) { - contentView.setTextViewText(R.id.info, mContext.getResources().getString( - R.string.status_bar_notification_info_overflow)); + contentView.setTextViewText(R.id.info, processLegacyText( + mContext.getResources().getString( + R.string.status_bar_notification_info_overflow))); } else { NumberFormat f = NumberFormat.getIntegerInstance(); - contentView.setTextViewText(R.id.info, f.format(mNumber)); + contentView.setTextViewText(R.id.info, processLegacyText(f.format(mNumber))); } contentView.setViewVisibility(R.id.info, View.VISIBLE); showLine3 = true; @@ -1762,9 +1911,9 @@ public class Notification implements Parcelable // Need to show three lines? if (mSubText != null) { - contentView.setTextViewText(R.id.text, mSubText); + contentView.setTextViewText(R.id.text, processLegacyText(mSubText)); if (mContentText != null) { - contentView.setTextViewText(R.id.text2, mContentText); + contentView.setTextViewText(R.id.text2, processLegacyText(mContentText)); contentView.setViewVisibility(R.id.text2, View.VISIBLE); showLine2 = true; } else { @@ -1835,7 +1984,7 @@ public class Notification implements Parcelable if (mContentView != null) { return mContentView; } else { - return applyStandardTemplate(R.layout.notification_template_base, true); // no more special large_icon flavor + return applyStandardTemplate(getBaseLayoutResource(), true); // no more special large_icon flavor } } @@ -1856,24 +2005,94 @@ public class Notification implements Parcelable private RemoteViews makeBigContentView() { if (mActions.size() == 0) return null; - return applyStandardTemplateWithActions(R.layout.notification_template_big_base); + return applyStandardTemplateWithActions(getBigBaseLayoutResource()); + } + + private RemoteViews makeHeadsUpContentView() { + if (mActions.size() == 0) return null; + + return applyStandardTemplateWithActions(getBigBaseLayoutResource()); } + private RemoteViews generateActionButton(Action action) { final boolean tombstone = (action.actionIntent == null); RemoteViews button = new RemoteViews(mContext.getPackageName(), - tombstone ? R.layout.notification_action_tombstone - : R.layout.notification_action); + tombstone ? getActionTombstoneLayoutResource() + : getActionLayoutResource()); button.setTextViewCompoundDrawablesRelative(R.id.action0, action.icon, 0, 0, 0); - button.setTextViewText(R.id.action0, action.title); + button.setTextViewText(R.id.action0, processLegacyText(action.title)); if (!tombstone) { button.setOnClickPendingIntent(R.id.action0, action.actionIntent); } button.setContentDescription(R.id.action0, action.title); + processLegacyAction(action, button); return button; } /** + * @return Whether we are currently building a notification from a legacy (an app that + * doesn't create quantum notifications by itself) app. + */ + private boolean isLegacy() { + return mLegacyNotificationUtil != null; + } + + private void processLegacyAction(Action action, RemoteViews button) { + if (isLegacy()) { + if (mLegacyNotificationUtil.isGrayscale(mContext, action.icon)) { + button.setTextViewCompoundDrawablesRelativeColorFilter(R.id.action0, 0, + mContext.getResources().getColor( + R.color.notification_action_legacy_color_filter), + PorterDuff.Mode.MULTIPLY); + } + } + } + + private CharSequence processLegacyText(CharSequence charSequence) { + if (isLegacy()) { + return mLegacyNotificationUtil.invertCharSequenceColors(charSequence); + } else { + return charSequence; + } + } + + private void processLegacyLargeIcon(int largeIconId, RemoteViews contentView) { + if (isLegacy()) { + processLegacyLargeIcon( + mLegacyNotificationUtil.isGrayscale(mContext, largeIconId), + contentView); + } + } + + private void processLegacyLargeIcon(Bitmap largeIcon, RemoteViews contentView) { + if (isLegacy()) { + processLegacyLargeIcon( + mLegacyNotificationUtil.isGrayscale(largeIcon), + contentView); + } + } + + private void processLegacyLargeIcon(boolean isGrayscale, RemoteViews contentView) { + if (isLegacy() && isGrayscale) { + contentView.setInt(R.id.icon, "setBackgroundResource", + R.drawable.notification_icon_legacy_bg_inset); + } + } + + private void processLegacySmallIcon(int smallIconDrawableId, int smallIconImageViewId, + RemoteViews contentView) { + if (isLegacy()) { + if (mLegacyNotificationUtil.isGrayscale(mContext, smallIconDrawableId)) { + contentView.setDrawableParameters(smallIconImageViewId, false, -1, + mContext.getResources().getColor( + R.color.notification_action_legacy_color_filter), + PorterDuff.Mode.MULTIPLY, -1); + } + } + } + + /** * Apply the unstyled operations and return a new {@link Notification} object. * @hide */ @@ -1899,6 +2118,7 @@ public class Notification implements Parcelable n.defaults = mDefaults; n.flags = mFlags; n.bigContentView = makeBigContentView(); + n.headsUpContentView = makeHeadsUpContentView(); if (mLedOnMs != 0 || mLedOffMs != 0) { n.flags |= FLAG_SHOW_LIGHTS; } @@ -1911,6 +2131,12 @@ public class Notification implements Parcelable n.actions = new Action[mActions.size()]; mActions.toArray(n.actions); } + n.visibility = mVisibility; + + if (mPublicVersion != null) { + n.publicVersion = new Notification(); + mPublicVersion.cloneInto(n.publicVersion, true); + } return n; } @@ -1935,6 +2161,9 @@ public class Notification implements Parcelable if (mLargeIcon != null) { extras.putParcelable(EXTRA_LARGE_ICON, mLargeIcon); } + if (!mPeople.isEmpty()) { + extras.putStringArray(EXTRA_PEOPLE, mPeople.toArray(new String[mPeople.size()])); + } } /** @@ -1975,6 +2204,49 @@ public class Notification implements Parcelable build().cloneInto(n, true); return n; } + + + private int getBaseLayoutResource() { + return mQuantumTheme + ? R.layout.notification_template_quantum_base + : R.layout.notification_template_base; + } + + private int getBigBaseLayoutResource() { + return mQuantumTheme + ? R.layout.notification_template_quantum_big_base + : R.layout.notification_template_big_base; + } + + private int getBigPictureLayoutResource() { + return mQuantumTheme + ? R.layout.notification_template_quantum_big_picture + : R.layout.notification_template_big_picture; + } + + private int getBigTextLayoutResource() { + return mQuantumTheme + ? R.layout.notification_template_quantum_big_text + : R.layout.notification_template_big_text; + } + + private int getInboxLayoutResource() { + return mQuantumTheme + ? R.layout.notification_template_quantum_inbox + : R.layout.notification_template_inbox; + } + + private int getActionLayoutResource() { + return mQuantumTheme + ? R.layout.notification_quantum_action + : R.layout.notification_action; + } + + private int getActionTombstoneLayoutResource() { + return mQuantumTheme + ? R.layout.notification_quantum_action_tombstone + : R.layout.notification_action_tombstone; + } } /** @@ -2039,7 +2311,7 @@ public class Notification implements Parcelable mSummaryTextSet ? mSummaryText : mBuilder.mSubText; if (overflowText != null) { - contentView.setTextViewText(R.id.text, overflowText); + contentView.setTextViewText(R.id.text, mBuilder.processLegacyText(overflowText)); contentView.setViewVisibility(R.id.overflow_divider, View.VISIBLE); contentView.setViewVisibility(R.id.line3, View.VISIBLE); } else { @@ -2060,6 +2332,7 @@ public class Notification implements Parcelable if (mBigContentTitle != null) { extras.putCharSequence(EXTRA_TITLE_BIG, mBigContentTitle); } + extras.putString(EXTRA_TEMPLATE, this.getClass().getName()); } /** @@ -2143,7 +2416,7 @@ public class Notification implements Parcelable } private RemoteViews makeBigContentView() { - RemoteViews contentView = getStandardView(R.layout.notification_template_big_picture); + RemoteViews contentView = getStandardView(mBuilder.getBigPictureLayoutResource()); contentView.setImageViewBitmap(R.id.big_picture, mPicture); @@ -2242,14 +2515,14 @@ public class Notification implements Parcelable final boolean hadThreeLines = (mBuilder.mContentText != null && mBuilder.mSubText != null); mBuilder.mContentText = null; - RemoteViews contentView = getStandardView(R.layout.notification_template_big_text); + RemoteViews contentView = getStandardView(mBuilder.getBigTextLayoutResource()); if (hadThreeLines) { // vertical centering contentView.setViewPadding(R.id.line1, 0, 0, 0, 0); } - contentView.setTextViewText(R.id.big_text, mBigText); + contentView.setTextViewText(R.id.big_text, mBuilder.processLegacyText(mBigText)); contentView.setViewVisibility(R.id.big_text, View.VISIBLE); contentView.setViewVisibility(R.id.text2, View.GONE); @@ -2336,7 +2609,7 @@ public class Notification implements Parcelable private RemoteViews makeBigContentView() { // Remove the content text so line3 disappears unless you have a summary mBuilder.mContentText = null; - RemoteViews contentView = getStandardView(R.layout.notification_template_inbox); + RemoteViews contentView = getStandardView(mBuilder.getInboxLayoutResource()); contentView.setViewVisibility(R.id.text2, View.GONE); @@ -2354,7 +2627,7 @@ public class Notification implements Parcelable CharSequence str = mTexts.get(i); if (str != null && !str.equals("")) { contentView.setViewVisibility(rowIds[i], View.VISIBLE); - contentView.setTextViewText(rowIds[i], str); + contentView.setTextViewText(rowIds[i], mBuilder.processLegacyText(str)); } i++; } diff --git a/core/java/android/app/OnActivityPausedListener.java b/core/java/android/app/OnActivityPausedListener.java index 379f133..5003973 100644 --- a/core/java/android/app/OnActivityPausedListener.java +++ b/core/java/android/app/OnActivityPausedListener.java @@ -5,7 +5,7 @@ * 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 + * 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, diff --git a/core/java/android/app/PackageInstallObserver.java b/core/java/android/app/PackageInstallObserver.java new file mode 100644 index 0000000..dacffb4 --- /dev/null +++ b/core/java/android/app/PackageInstallObserver.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2014 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.app; + +import android.content.pm.IPackageInstallObserver2; +import android.os.Bundle; +import android.os.RemoteException; + +/** + * @hide + * + * New-style observer for package installers to use. + */ +public class PackageInstallObserver { + IPackageInstallObserver2.Stub mObserver = new IPackageInstallObserver2.Stub() { + @Override + public void packageInstalled(String pkgName, Bundle extras, int result) + throws RemoteException { + PackageInstallObserver.this.packageInstalled(pkgName, extras, result); + } + }; + + /** + * This method will be called to report the result of the package installation attempt. + * + * @param pkgName Name of the package whose installation was attempted + * @param extras If non-null, this Bundle contains extras providing additional information + * about an install failure. See {@link android.content.pm.PackageManager} for + * documentation about which extras apply to various failures; in particular the + * strings named EXTRA_FAILURE_*. + * @param result The numeric success or failure code indicating the basic outcome + */ + public void packageInstalled(String pkgName, Bundle extras, int result) { + } +} diff --git a/core/java/android/app/PendingIntent.java b/core/java/android/app/PendingIntent.java index 7129e9e..cf14202 100644 --- a/core/java/android/app/PendingIntent.java +++ b/core/java/android/app/PendingIntent.java @@ -16,6 +16,9 @@ package android.app; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; import android.content.Intent; import android.content.IIntentReceiver; @@ -30,6 +33,9 @@ import android.os.Parcelable; import android.os.UserHandle; import android.util.AndroidException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * A description of an Intent and target action to perform with it. Instances * of this class are created with {@link #getActivity}, {@link #getActivities}, @@ -86,6 +92,26 @@ import android.util.AndroidException; public final class PendingIntent implements Parcelable { private final IIntentSender mTarget; + /** @hide */ + @IntDef(flag = true, + value = { + FLAG_ONE_SHOT, + FLAG_NO_CREATE, + FLAG_CANCEL_CURRENT, + FLAG_UPDATE_CURRENT, + + Intent.FILL_IN_ACTION, + Intent.FILL_IN_DATA, + Intent.FILL_IN_CATEGORIES, + Intent.FILL_IN_COMPONENT, + Intent.FILL_IN_PACKAGE, + Intent.FILL_IN_SOURCE_BOUNDS, + Intent.FILL_IN_SELECTOR, + Intent.FILL_IN_CLIP_DATA + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Flags {} + /** * Flag indicating that this PendingIntent can be used only once. * For use with {@link #getActivity}, {@link #getBroadcast}, and @@ -222,7 +248,7 @@ public final class PendingIntent implements Parcelable { * supplied. */ public static PendingIntent getActivity(Context context, int requestCode, - Intent intent, int flags) { + Intent intent, @Flags int flags) { return getActivity(context, requestCode, intent, flags, null); } @@ -255,7 +281,7 @@ public final class PendingIntent implements Parcelable { * supplied. */ public static PendingIntent getActivity(Context context, int requestCode, - Intent intent, int flags, Bundle options) { + @NonNull Intent intent, @Flags int flags, @Nullable Bundle options) { String packageName = context.getPackageName(); String resolvedType = intent != null ? intent.resolveTypeIfNeeded( context.getContentResolver()) : null; @@ -280,7 +306,7 @@ public final class PendingIntent implements Parcelable { * activity is started, not when the pending intent is created. */ public static PendingIntent getActivityAsUser(Context context, int requestCode, - Intent intent, int flags, Bundle options, UserHandle user) { + @NonNull Intent intent, int flags, Bundle options, UserHandle user) { String packageName = context.getPackageName(); String resolvedType = intent != null ? intent.resolveTypeIfNeeded( context.getContentResolver()) : null; @@ -345,7 +371,7 @@ public final class PendingIntent implements Parcelable { * supplied. */ public static PendingIntent getActivities(Context context, int requestCode, - Intent[] intents, int flags) { + @NonNull Intent[] intents, @Flags int flags) { return getActivities(context, requestCode, intents, flags, null); } @@ -395,7 +421,7 @@ public final class PendingIntent implements Parcelable { * supplied. */ public static PendingIntent getActivities(Context context, int requestCode, - Intent[] intents, int flags, Bundle options) { + @NonNull Intent[] intents, @Flags int flags, @Nullable Bundle options) { String packageName = context.getPackageName(); String[] resolvedTypes = new String[intents.length]; for (int i=0; i<intents.length; i++) { @@ -421,7 +447,7 @@ public final class PendingIntent implements Parcelable { * activity is started, not when the pending intent is created. */ public static PendingIntent getActivitiesAsUser(Context context, int requestCode, - Intent[] intents, int flags, Bundle options, UserHandle user) { + @NonNull Intent[] intents, int flags, Bundle options, UserHandle user) { String packageName = context.getPackageName(); String[] resolvedTypes = new String[intents.length]; for (int i=0; i<intents.length; i++) { @@ -465,7 +491,7 @@ public final class PendingIntent implements Parcelable { * supplied. */ public static PendingIntent getBroadcast(Context context, int requestCode, - Intent intent, int flags) { + Intent intent, @Flags int flags) { return getBroadcastAsUser(context, requestCode, intent, flags, new UserHandle(UserHandle.myUserId())); } @@ -519,7 +545,7 @@ public final class PendingIntent implements Parcelable { * supplied. */ public static PendingIntent getService(Context context, int requestCode, - Intent intent, int flags) { + @NonNull Intent intent, @Flags int flags) { String packageName = context.getPackageName(); String resolvedType = intent != null ? intent.resolveTypeIfNeeded( context.getContentResolver()) : null; @@ -749,6 +775,7 @@ public final class PendingIntent implements Parcelable { * @return The package name of the PendingIntent, or null if there is * none associated with it. */ + @Nullable public String getCreatorPackage() { try { return ActivityManagerNative.getDefault() @@ -807,6 +834,7 @@ public final class PendingIntent implements Parcelable { * @return The user handle of the PendingIntent, or null if there is * none associated with it. */ + @Nullable public UserHandle getCreatorUserHandle() { try { int uid = ActivityManagerNative.getDefault() @@ -861,6 +889,20 @@ public final class PendingIntent implements Parcelable { } /** + * @hide + * Return descriptive tag for this PendingIntent. + */ + public String getTag(String prefix) { + try { + return ActivityManagerNative.getDefault() + .getTagForIntentSender(mTarget, prefix); + } catch (RemoteException e) { + // Should never happen. + return null; + } + } + + /** * Comparison operator on two PendingIntent objects, such that true * is returned then they both represent the same operation from the * same package. This allows you to use {@link #getActivity}, @@ -922,8 +964,8 @@ public final class PendingIntent implements Parcelable { * @param sender The PendingIntent to write, or null. * @param out Where to write the PendingIntent. */ - public static void writePendingIntentOrNullToParcel(PendingIntent sender, - Parcel out) { + public static void writePendingIntentOrNullToParcel(@Nullable PendingIntent sender, + @NonNull Parcel out) { out.writeStrongBinder(sender != null ? sender.mTarget.asBinder() : null); } @@ -938,7 +980,8 @@ public final class PendingIntent implements Parcelable { * @return Returns the Messenger read from the Parcel, or null if null had * been written. */ - public static PendingIntent readPendingIntentOrNullFromParcel(Parcel in) { + @Nullable + public static PendingIntent readPendingIntentOrNullFromParcel(@NonNull Parcel in) { IBinder b = in.readStrongBinder(); return b != null ? new PendingIntent(b) : null; } diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java index 728f372..a67faa0 100644 --- a/core/java/android/app/ResourcesManager.java +++ b/core/java/android/app/ResourcesManager.java @@ -144,14 +144,16 @@ public class ResourcesManager { * Creates the top level Resources for applications with the given compatibility info. * * @param resDir the resource directory. + * @param overlayDirs the resource overlay directories. + * @param libDirs the shared library resource dirs this app references. * @param compatInfo the compability info. Must not be null. * @param token the application token for determining stack bounds. */ - public Resources getTopLevelResources(String resDir, String[] overlayDirs, int displayId, - Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token) { + public Resources getTopLevelResources(String resDir, String[] overlayDirs, String[] libDirs, + int displayId, Configuration overrideConfiguration, CompatibilityInfo compatInfo, + IBinder token) { final float scale = compatInfo.applicationScale; - ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfiguration, scale, - token); + ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfiguration, scale, token); Resources r; synchronized (this) { // Resources is app scale dependent. @@ -186,6 +188,15 @@ public class ResourcesManager { } } + if (libDirs != null) { + for (String libDir : libDirs) { + if (assets.addAssetPath(libDir) == 0) { + Slog.w(TAG, "Asset path '" + libDir + + "' does not exist or contains no resources."); + } + } + } + //Slog.i(TAG, "Resource: key=" + key + ", display metrics=" + metrics); DisplayMetrics dm = getDisplayMetricsLocked(displayId); Configuration config; diff --git a/core/java/android/app/ResultInfo.java b/core/java/android/app/ResultInfo.java index 48a0fc2..5e0867c 100644 --- a/core/java/android/app/ResultInfo.java +++ b/core/java/android/app/ResultInfo.java @@ -17,12 +17,8 @@ package android.app; import android.content.Intent; -import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; -import android.os.Bundle; - -import java.util.Map; /** * {@hide} diff --git a/core/java/android/app/SearchDialog.java b/core/java/android/app/SearchDialog.java index d04e9db..af1810b 100644 --- a/core/java/android/app/SearchDialog.java +++ b/core/java/android/app/SearchDialog.java @@ -188,8 +188,7 @@ public class SearchDialog extends Dialog { mSearchView.findViewById(com.android.internal.R.id.search_src_text); mAppIcon = (ImageView) findViewById(com.android.internal.R.id.search_app_icon); mSearchPlate = mSearchView.findViewById(com.android.internal.R.id.search_plate); - mWorkingSpinner = getContext().getResources(). - getDrawable(com.android.internal.R.drawable.search_spinner); + mWorkingSpinner = getContext().getDrawable(com.android.internal.R.drawable.search_spinner); // TODO: Restore the spinner for slow suggestion lookups // mSearchAutoComplete.setCompoundDrawablesWithIntrinsicBounds( // null, null, mWorkingSpinner, null); @@ -458,7 +457,7 @@ public class SearchDialog extends Dialog { // optionally show one or the other. if (mSearchable.useBadgeIcon()) { - icon = mActivityContext.getResources().getDrawable(mSearchable.getIconId()); + icon = mActivityContext.getDrawable(mSearchable.getIconId()); visibility = View.VISIBLE; if (DBG) Log.d(LOG_TAG, "Using badge icon: " + mSearchable.getIconId()); } else if (mSearchable.useBadgeLabel()) { diff --git a/core/java/android/app/SearchManager.java b/core/java/android/app/SearchManager.java index f9c245e..33c3409 100644 --- a/core/java/android/app/SearchManager.java +++ b/core/java/android/app/SearchManager.java @@ -22,7 +22,6 @@ import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; -import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.database.Cursor; import android.graphics.Rect; @@ -34,7 +33,6 @@ import android.os.ServiceManager; import android.os.UserHandle; import android.text.TextUtils; import android.util.Log; -import android.util.Slog; import android.view.KeyEvent; import java.util.List; diff --git a/core/java/android/app/SharedPreferencesImpl.java b/core/java/android/app/SharedPreferencesImpl.java index a292ecb..1b838fb 100644 --- a/core/java/android/app/SharedPreferencesImpl.java +++ b/core/java/android/app/SharedPreferencesImpl.java @@ -42,7 +42,6 @@ import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; import libcore.io.ErrnoException; import libcore.io.IoUtils; diff --git a/core/java/android/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java index 2045ed8..a6a04d1 100644 --- a/core/java/android/app/StatusBarManager.java +++ b/core/java/android/app/StatusBarManager.java @@ -38,8 +38,11 @@ public class StatusBarManager { public static final int DISABLE_NOTIFICATION_ICONS = View.STATUS_BAR_DISABLE_NOTIFICATION_ICONS; public static final int DISABLE_NOTIFICATION_ALERTS = View.STATUS_BAR_DISABLE_NOTIFICATION_ALERTS; + @Deprecated public static final int DISABLE_NOTIFICATION_TICKER = View.STATUS_BAR_DISABLE_NOTIFICATION_TICKER; + public static final int DISABLE_PRIVATE_NOTIFICATIONS + = View.STATUS_BAR_DISABLE_NOTIFICATION_TICKER; public static final int DISABLE_SYSTEM_INFO = View.STATUS_BAR_DISABLE_SYSTEM_INFO; public static final int DISABLE_HOME = View.STATUS_BAR_DISABLE_HOME; public static final int DISABLE_RECENT = View.STATUS_BAR_DISABLE_RECENT; diff --git a/core/java/android/app/TaskStackBuilder.java b/core/java/android/app/TaskStackBuilder.java index 3e0ac7e..0077db1 100644 --- a/core/java/android/app/TaskStackBuilder.java +++ b/core/java/android/app/TaskStackBuilder.java @@ -16,6 +16,7 @@ package android.app; +import android.annotation.NonNull; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -244,7 +245,7 @@ public class TaskStackBuilder { * * @return The obtained PendingIntent */ - public PendingIntent getPendingIntent(int requestCode, int flags) { + public PendingIntent getPendingIntent(int requestCode, @PendingIntent.Flags int flags) { return getPendingIntent(requestCode, flags, null); } @@ -263,7 +264,8 @@ public class TaskStackBuilder { * * @return The obtained PendingIntent */ - public PendingIntent getPendingIntent(int requestCode, int flags, Bundle options) { + public PendingIntent getPendingIntent(int requestCode, @PendingIntent.Flags int flags, + Bundle options) { if (mIntents.isEmpty()) { throw new IllegalStateException( "No intents added to TaskStackBuilder; cannot getPendingIntent"); @@ -294,6 +296,7 @@ public class TaskStackBuilder { * * @return An array containing the intents added to this builder. */ + @NonNull public Intent[] getIntents() { Intent[] intents = new Intent[mIntents.size()]; if (intents.length == 0) return intents; diff --git a/core/java/android/app/TimePickerDialog.java b/core/java/android/app/TimePickerDialog.java index 952227f..a85c61f 100644 --- a/core/java/android/app/TimePickerDialog.java +++ b/core/java/android/app/TimePickerDialog.java @@ -16,17 +16,19 @@ package android.app; -import com.android.internal.R; - import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.os.Bundle; +import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.widget.TimePicker; import android.widget.TimePicker.OnTimeChangedListener; +import com.android.internal.R; + + /** * A dialog that prompts the user for the time of day using a {@link TimePicker}. * @@ -38,7 +40,7 @@ public class TimePickerDialog extends AlertDialog /** * The callback interface used to indicate the user is done filling in - * the time (they clicked on the 'Set' button). + * the time (they clicked on the 'Done' button). */ public interface OnTimeSetListener { @@ -55,7 +57,7 @@ public class TimePickerDialog extends AlertDialog private static final String IS_24_HOUR = "is24hour"; private final TimePicker mTimePicker; - private final OnTimeSetListener mCallback; + private final OnTimeSetListener mTimeSetCallback; int mInitialHourOfDay; int mInitialMinute; @@ -74,6 +76,16 @@ public class TimePickerDialog extends AlertDialog this(context, 0, callBack, hourOfDay, minute, is24HourView); } + static int resolveDialogTheme(Context context, int resid) { + if (resid == 0) { + TypedValue outValue = new TypedValue(); + context.getTheme().resolveAttribute(R.attr.timePickerDialogTheme, outValue, true); + return outValue.resourceId; + } else { + return resid; + } + } + /** * @param context Parent. * @param theme the theme to apply to this dialog @@ -86,17 +98,13 @@ public class TimePickerDialog extends AlertDialog int theme, OnTimeSetListener callBack, int hourOfDay, int minute, boolean is24HourView) { - super(context, theme); - mCallback = callBack; + super(context, resolveDialogTheme(context, theme)); + mTimeSetCallback = callBack; mInitialHourOfDay = hourOfDay; mInitialMinute = minute; mIs24HourView = is24HourView; - setIcon(0); - setTitle(R.string.time_picker_dialog_title); - Context themeContext = getContext(); - setButton(BUTTON_POSITIVE, themeContext.getText(R.string.date_time_done), this); LayoutInflater inflater = (LayoutInflater) themeContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); @@ -104,7 +112,18 @@ public class TimePickerDialog extends AlertDialog setView(view); mTimePicker = (TimePicker) view.findViewById(R.id.timePicker); - // initialize state + // Initialize state + mTimePicker.setLegacyMode(false /* will show new UI */); + mTimePicker.setShowDoneButton(true); + mTimePicker.setDismissCallback(new TimePicker.TimePickerDismissCallback() { + @Override + public void dismiss(TimePicker view, boolean isCancel, int hourOfDay, int minute) { + if (!isCancel) { + mTimeSetCallback.onTimeSet(view, hourOfDay, minute); + } + TimePickerDialog.this.dismiss(); + } + }); mTimePicker.setIs24HourView(mIs24HourView); mTimePicker.setCurrentHour(mInitialHourOfDay); mTimePicker.setCurrentMinute(mInitialMinute); @@ -125,9 +144,9 @@ public class TimePickerDialog extends AlertDialog } private void tryNotifyTimeSet() { - if (mCallback != null) { + if (mTimeSetCallback != null) { mTimePicker.clearFocus(); - mCallback.onTimeSet(mTimePicker, mTimePicker.getCurrentHour(), + mTimeSetCallback.onTimeSet(mTimePicker, mTimePicker.getCurrentHour(), mTimePicker.getCurrentMinute()); } } diff --git a/core/java/android/app/UiAutomation.java b/core/java/android/app/UiAutomation.java index 498fa42..9405325 100644 --- a/core/java/android/app/UiAutomation.java +++ b/core/java/android/app/UiAutomation.java @@ -33,12 +33,16 @@ import android.view.Display; import android.view.InputEvent; import android.view.KeyEvent; import android.view.Surface; +import android.view.WindowAnimationFrameStats; +import android.view.WindowContentFrameStats; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityInteractionClient; import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityWindowInfo; import android.view.accessibility.IAccessibilityInteractionConnection; import java.util.ArrayList; +import java.util.List; import java.util.concurrent.TimeoutException; /** @@ -269,10 +273,10 @@ public final class UiAutomation { * @param action The action to perform. * @return Whether the action was successfully performed. * - * @see AccessibilityService#GLOBAL_ACTION_BACK - * @see AccessibilityService#GLOBAL_ACTION_HOME - * @see AccessibilityService#GLOBAL_ACTION_NOTIFICATIONS - * @see AccessibilityService#GLOBAL_ACTION_RECENTS + * @see android.accessibilityservice.AccessibilityService#GLOBAL_ACTION_BACK + * @see android.accessibilityservice.AccessibilityService#GLOBAL_ACTION_HOME + * @see android.accessibilityservice.AccessibilityService#GLOBAL_ACTION_NOTIFICATIONS + * @see android.accessibilityservice.AccessibilityService#GLOBAL_ACTION_RECENTS */ public final boolean performGlobalAction(int action) { final IAccessibilityServiceConnection connection; @@ -293,6 +297,28 @@ public final class UiAutomation { } /** + * Find the view that has the specified focus type. The search is performed + * across all windows. + * <p> + * <strong>Note:</strong> In order to access the windows you have to opt-in + * to retrieve the interactive windows by setting the + * {@link AccessibilityServiceInfo#FLAG_RETRIEVE_INTERACTIVE_WINDOWS} flag. + * Otherwise, the search will be performed only in the active window. + * </p> + * + * @param focus The focus to find. One of {@link AccessibilityNodeInfo#FOCUS_INPUT} or + * {@link AccessibilityNodeInfo#FOCUS_ACCESSIBILITY}. + * @return The node info of the focused view or null. + * + * @see AccessibilityNodeInfo#FOCUS_INPUT + * @see AccessibilityNodeInfo#FOCUS_ACCESSIBILITY + */ + public AccessibilityNodeInfo findFocus(int focus) { + return AccessibilityInteractionClient.getInstance().findFocus(mConnectionId, + AccessibilityNodeInfo.ANY_WINDOW_ID, AccessibilityNodeInfo.ROOT_NODE_ID, focus); + } + + /** * Gets the an {@link AccessibilityServiceInfo} describing this UiAutomation. * This method is useful if one wants to change some of the dynamically * configurable properties at runtime. @@ -346,6 +372,33 @@ public final class UiAutomation { } /** + * Gets the windows on the screen. This method returns only the windows + * that a sighted user can interact with, as opposed to all windows. + * For example, if there is a modal dialog shown and the user cannot touch + * anything behind it, then only the modal window will be reported + * (assuming it is the top one). For convenience the returned windows + * are ordered in a descending layer order, which is the windows that + * are higher in the Z-order are reported first. + * <p> + * <strong>Note:</strong> In order to access the windows you have to opt-in + * to retrieve the interactive windows by setting the + * {@link AccessibilityServiceInfo#FLAG_RETRIEVE_INTERACTIVE_WINDOWS} flag. + * </p> + * + * @return The windows if there are windows such, otherwise an empty list. + */ + public List<AccessibilityWindowInfo> getWindows() { + final int connectionId; + synchronized (mLock) { + throwIfNotConnectedLocked(); + connectionId = mConnectionId; + } + // Calling out without a lock held. + return AccessibilityInteractionClient.getInstance() + .getWindows(connectionId); + } + + /** * Gets the root {@link AccessibilityNodeInfo} in the active window. * * @return The root info. @@ -632,7 +685,7 @@ public final class UiAutomation { * potentially undesirable actions such as calling 911 or posting on public forums etc. * * @param enable whether to run in a "monkey" mode or not. Default is not. - * @see {@link ActivityManager#isUserAMonkey()} + * @see {@link android.app.ActivityManager#isUserAMonkey()} */ public void setRunAsMonkey(boolean enable) { synchronized (mLock) { @@ -645,6 +698,148 @@ public final class UiAutomation { } } + /** + * Clears the frame statistics for the content of a given window. These + * statistics contain information about the most recently rendered content + * frames. + * + * @param windowId The window id. + * @return Whether the window is present and its frame statistics + * were cleared. + * + * @see android.view.WindowContentFrameStats + * @see #getWindowContentFrameStats(int) + * @see #getWindows() + * @see AccessibilityWindowInfo#getId() AccessibilityWindowInfo.getId() + */ + public boolean clearWindowContentFrameStats(int windowId) { + synchronized (mLock) { + throwIfNotConnectedLocked(); + } + try { + if (DEBUG) { + Log.i(LOG_TAG, "Clearing content frame stats for window: " + windowId); + } + // Calling out without a lock held. + return mUiAutomationConnection.clearWindowContentFrameStats(windowId); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error clearing window content frame stats!", re); + } + return false; + } + + /** + * Gets the frame statistics for a given window. These statistics contain + * information about the most recently rendered content frames. + * <p> + * A typical usage requires clearing the window frame statistics via {@link + * #clearWindowContentFrameStats(int)} followed by an interaction with the UI and + * finally getting the window frame statistics via calling this method. + * </p> + * <pre> + * // Assume we have at least one window. + * final int windowId = getWindows().get(0).getId(); + * + * // Start with a clean slate. + * uiAutimation.clearWindowContentFrameStats(windowId); + * + * // Do stuff with the UI. + * + * // Get the frame statistics. + * WindowContentFrameStats stats = uiAutomation.getWindowContentFrameStats(windowId); + * </pre> + * + * @param windowId The window id. + * @return The window frame statistics, or null if the window is not present. + * + * @see android.view.WindowContentFrameStats + * @see #clearWindowContentFrameStats(int) + * @see #getWindows() + * @see AccessibilityWindowInfo#getId() AccessibilityWindowInfo.getId() + */ + public WindowContentFrameStats getWindowContentFrameStats(int windowId) { + synchronized (mLock) { + throwIfNotConnectedLocked(); + } + try { + if (DEBUG) { + Log.i(LOG_TAG, "Getting content frame stats for window: " + windowId); + } + // Calling out without a lock held. + return mUiAutomationConnection.getWindowContentFrameStats(windowId); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error getting window content frame stats!", re); + } + return null; + } + + /** + * Clears the window animation rendering statistics. These statistics contain + * information about the most recently rendered window animation frames, i.e. + * for window transition animations. + * + * @see android.view.WindowAnimationFrameStats + * @see #getWindowAnimationFrameStats() + * @see android.R.styleable#WindowAnimation + */ + public void clearWindowAnimationFrameStats() { + synchronized (mLock) { + throwIfNotConnectedLocked(); + } + try { + if (DEBUG) { + Log.i(LOG_TAG, "Clearing window animation frame stats"); + } + // Calling out without a lock held. + mUiAutomationConnection.clearWindowAnimationFrameStats(); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error clearing window animation frame stats!", re); + } + } + + /** + * Gets the window animation frame statistics. These statistics contain + * information about the most recently rendered window animation frames, i.e. + * for window transition animations. + * + * <p> + * A typical usage requires clearing the window animation frame statistics via + * {@link #clearWindowAnimationFrameStats()} followed by an interaction that causes + * a window transition which uses a window animation and finally getting the window + * animation frame statistics by calling this method. + * </p> + * <pre> + * // Start with a clean slate. + * uiAutimation.clearWindowAnimationFrameStats(); + * + * // Do stuff to trigger a window transition. + * + * // Get the frame statistics. + * WindowAnimationFrameStats stats = uiAutomation.getWindowAnimationFrameStats(); + * </pre> + * + * @return The window animation frame statistics. + * + * @see android.view.WindowAnimationFrameStats + * @see #clearWindowAnimationFrameStats() + * @see android.R.styleable#WindowAnimation + */ + public WindowAnimationFrameStats getWindowAnimationFrameStats() { + synchronized (mLock) { + throwIfNotConnectedLocked(); + } + try { + if (DEBUG) { + Log.i(LOG_TAG, "Getting window animation frame stats"); + } + // Calling out without a lock held. + return mUiAutomationConnection.getWindowAnimationFrameStats(); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error getting window animation frame stats!", re); + } + return null; + } + private static float getDegreesForRotation(int value) { switch (value) { case Surface.ROTATION_90: { diff --git a/core/java/android/app/UiAutomationConnection.java b/core/java/android/app/UiAutomationConnection.java index 91b0d7c..fa40286 100644 --- a/core/java/android/app/UiAutomationConnection.java +++ b/core/java/android/app/UiAutomationConnection.java @@ -22,12 +22,15 @@ import android.content.Context; import android.graphics.Bitmap; import android.hardware.input.InputManager; import android.os.Binder; +import android.os.IBinder; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; import android.view.IWindowManager; import android.view.InputEvent; import android.view.SurfaceControl; +import android.view.WindowAnimationFrameStats; +import android.view.WindowContentFrameStats; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.IAccessibilityManager; @@ -47,6 +50,9 @@ public final class UiAutomationConnection extends IUiAutomationConnection.Stub { private final IWindowManager mWindowManager = IWindowManager.Stub.asInterface( ServiceManager.getService(Service.WINDOW_SERVICE)); + private final IAccessibilityManager mAccessibilityManager = IAccessibilityManager.Stub.asInterface( + ServiceManager.getService(Service.ACCESSIBILITY_SERVICE)); + private final Object mLock = new Object(); private final Binder mToken = new Binder(); @@ -144,6 +150,76 @@ public final class UiAutomationConnection extends IUiAutomationConnection.Stub { } @Override + public boolean clearWindowContentFrameStats(int windowId) throws RemoteException { + synchronized (mLock) { + throwIfCalledByNotTrustedUidLocked(); + throwIfShutdownLocked(); + throwIfNotConnectedLocked(); + } + final long identity = Binder.clearCallingIdentity(); + try { + IBinder token = mAccessibilityManager.getWindowToken(windowId); + if (token == null) { + return false; + } + return mWindowManager.clearWindowContentFrameStats(token); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public WindowContentFrameStats getWindowContentFrameStats(int windowId) throws RemoteException { + synchronized (mLock) { + throwIfCalledByNotTrustedUidLocked(); + throwIfShutdownLocked(); + throwIfNotConnectedLocked(); + } + final long identity = Binder.clearCallingIdentity(); + try { + IBinder token = mAccessibilityManager.getWindowToken(windowId); + if (token == null) { + return null; + } + return mWindowManager.getWindowContentFrameStats(token); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void clearWindowAnimationFrameStats() { + synchronized (mLock) { + throwIfCalledByNotTrustedUidLocked(); + throwIfShutdownLocked(); + throwIfNotConnectedLocked(); + } + final long identity = Binder.clearCallingIdentity(); + try { + SurfaceControl.clearAnimationFrameStats(); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public WindowAnimationFrameStats getWindowAnimationFrameStats() { + synchronized (mLock) { + throwIfCalledByNotTrustedUidLocked(); + throwIfShutdownLocked(); + throwIfNotConnectedLocked(); + } + final long identity = Binder.clearCallingIdentity(); + try { + WindowAnimationFrameStats stats = new WindowAnimationFrameStats(); + SurfaceControl.getAnimationFrameStats(stats); + return stats; + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override public void shutdown() { synchronized (mLock) { if (isConnectedLocked()) { diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java index f291e82..e6e0f35 100644 --- a/core/java/android/app/WallpaperManager.java +++ b/core/java/android/app/WallpaperManager.java @@ -41,7 +41,6 @@ import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; -import android.os.Message; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.ServiceManager; @@ -219,24 +218,9 @@ public class WallpaperManager { private static final int MSG_CLEAR_WALLPAPER = 1; - private final Handler mHandler; - Globals(Looper looper) { IBinder b = ServiceManager.getService(Context.WALLPAPER_SERVICE); mService = IWallpaperManager.Stub.asInterface(b); - mHandler = new Handler(looper) { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MSG_CLEAR_WALLPAPER: - synchronized (this) { - mWallpaper = null; - mDefaultWallpaper = null; - } - break; - } - } - }; } public void onWallpaperChanged() { @@ -245,7 +229,10 @@ public class WallpaperManager { * to null so if the user requests the wallpaper again then we'll * fetch it. */ - mHandler.sendEmptyMessage(MSG_CLEAR_WALLPAPER); + synchronized (this) { + mWallpaper = null; + mDefaultWallpaper = null; + } } public Bitmap peekWallpaperBitmap(Context context, boolean returnDefault) { @@ -278,7 +265,6 @@ public class WallpaperManager { synchronized (this) { mWallpaper = null; mDefaultWallpaper = null; - mHandler.removeMessages(MSG_CLEAR_WALLPAPER); } } diff --git a/core/java/android/app/admin/DeviceAdminInfo.java b/core/java/android/app/admin/DeviceAdminInfo.java index 66fc816..3074b49 100644 --- a/core/java/android/app/admin/DeviceAdminInfo.java +++ b/core/java/android/app/admin/DeviceAdminInfo.java @@ -52,6 +52,22 @@ public final class DeviceAdminInfo implements Parcelable { static final String TAG = "DeviceAdminInfo"; /** + * A type of policy that this device admin can use: device owner meta-policy + * for an admin that is designated as owner of the device. + * + * @hide + */ + public static final int USES_POLICY_DEVICE_OWNER = -2; + + /** + * A type of policy that this device admin can use: profile owner meta-policy + * for admins that have been installed as owner of some user profile. + * + * @hide + */ + public static final int USES_POLICY_PROFILE_OWNER = -1; + + /** * A type of policy that this device admin can use: limit the passwords * that the user can select, via {@link DevicePolicyManager#setPasswordQuality} * and {@link DevicePolicyManager#setPasswordMinimumLength}. diff --git a/core/java/android/app/admin/DeviceAdminReceiver.java b/core/java/android/app/admin/DeviceAdminReceiver.java index 30b65de..f9d9059 100644 --- a/core/java/android/app/admin/DeviceAdminReceiver.java +++ b/core/java/android/app/admin/DeviceAdminReceiver.java @@ -29,25 +29,25 @@ import android.os.Bundle; * Base class for implementing a device administration component. This * class provides a convenience for interpreting the raw intent actions * that are sent by the system. - * + * * <p>The callback methods, like the base * {@link BroadcastReceiver#onReceive(Context, Intent) BroadcastReceiver.onReceive()} * method, happen on the main thread of the process. Thus long running * operations must be done on another thread. Note that because a receiver * is done once returning from its receive function, such long-running operations * should probably be done in a {@link Service}. - * + * * <p>When publishing your DeviceAdmin subclass as a receiver, it must * handle {@link #ACTION_DEVICE_ADMIN_ENABLED} and require the * {@link android.Manifest.permission#BIND_DEVICE_ADMIN} permission. A typical * manifest entry would look like:</p> - * + * * {@sample development/samples/ApiDemos/AndroidManifest.xml device_admin_declaration} - * + * * <p>The meta-data referenced here provides addition information specific * to the device administrator, as parsed by the {@link DeviceAdminInfo} class. * A typical file would be:</p> - * + * * {@sample development/samples/ApiDemos/res/xml/device_admin_sample.xml meta_data} * * <div class="special reference"> @@ -86,7 +86,7 @@ public class DeviceAdminReceiver extends BroadcastReceiver { @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_DEVICE_ADMIN_DISABLE_REQUESTED = "android.app.action.DEVICE_ADMIN_DISABLE_REQUESTED"; - + /** * A CharSequence that can be shown to the user informing them of the * impact of disabling your admin. @@ -94,7 +94,7 @@ public class DeviceAdminReceiver extends BroadcastReceiver { * @see #ACTION_DEVICE_ADMIN_DISABLE_REQUESTED */ public static final String EXTRA_DISABLE_WARNING = "android.app.extra.DISABLE_WARNING"; - + /** * Action sent to a device administrator when the user has disabled * it. Upon return, the application no longer has access to the @@ -107,7 +107,7 @@ public class DeviceAdminReceiver extends BroadcastReceiver { @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_DEVICE_ADMIN_DISABLED = "android.app.action.DEVICE_ADMIN_DISABLED"; - + /** * Action sent to a device administrator when the user has changed the * password of their device. You can at this point check the characteristics @@ -115,7 +115,7 @@ public class DeviceAdminReceiver extends BroadcastReceiver { * DevicePolicyManager.isActivePasswordSufficient()}. * You will generally * handle this in {@link DeviceAdminReceiver#onPasswordChanged}. - * + * * <p>The calling device admin must have requested * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to receive * this broadcast. @@ -123,7 +123,7 @@ public class DeviceAdminReceiver extends BroadcastReceiver { @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_PASSWORD_CHANGED = "android.app.action.ACTION_PASSWORD_CHANGED"; - + /** * Action sent to a device administrator when the user has failed at * attempted to enter the password. You can at this point check the @@ -131,7 +131,7 @@ public class DeviceAdminReceiver extends BroadcastReceiver { * {@link DevicePolicyManager#getCurrentFailedPasswordAttempts * DevicePolicyManager.getCurrentFailedPasswordAttempts()}. You will generally * handle this in {@link DeviceAdminReceiver#onPasswordFailed}. - * + * * <p>The calling device admin must have requested * {@link DeviceAdminInfo#USES_POLICY_WATCH_LOGIN} to receive * this broadcast. @@ -139,11 +139,11 @@ public class DeviceAdminReceiver extends BroadcastReceiver { @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_PASSWORD_FAILED = "android.app.action.ACTION_PASSWORD_FAILED"; - + /** * Action sent to a device administrator when the user has successfully * entered their password, after failing one or more times. - * + * * <p>The calling device admin must have requested * {@link DeviceAdminInfo#USES_POLICY_WATCH_LOGIN} to receive * this broadcast. @@ -165,15 +165,31 @@ public class DeviceAdminReceiver extends BroadcastReceiver { = "android.app.action.ACTION_PASSWORD_EXPIRING"; /** + * Broadcast Action: This broadcast is sent to the newly created profile when + * the provisioning of a managed profile has completed successfully. + * + * <p>The broadcast is limited to the package which started the provisioning as specified in + * the extra {@link DevicePolicyManager#EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME} of the + * {@link DevicePolicyManager#ACTION_PROVISION_MANAGED_PROFILE} intent that started the + * provisioning. It is also limited to the managed profile. + * + * <p>Input: Nothing.</p> + * <p>Output: Nothing</p> + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_PROFILE_PROVISIONING_COMPLETE = + "android.app.action.ACTION_PROFILE_PROVISIONING_COMPLETE"; + + /** * Name under which a DevicePolicy component publishes information * about itself. This meta-data must reference an XML resource containing * a device-admin tag. XXX TO DO: describe syntax. */ public static final String DEVICE_ADMIN_META_DATA = "android.app.device_admin"; - + private DevicePolicyManager mManager; private ComponentName mWho; - + /** * Retrieve the DevicePolicyManager interface for this administrator to work * with the system. @@ -186,7 +202,7 @@ public class DeviceAdminReceiver extends BroadcastReceiver { Context.DEVICE_POLICY_SERVICE); return mManager; } - + /** * Retrieve the ComponentName describing who this device administrator is, for * use in {@link DevicePolicyManager} APIs that require the administrator to @@ -199,7 +215,7 @@ public class DeviceAdminReceiver extends BroadcastReceiver { mWho = new ComponentName(context, getClass()); return mWho; } - + /** * Called after the administrator is first enabled, as a result of * receiving {@link #ACTION_DEVICE_ADMIN_ENABLED}. At this point you @@ -209,7 +225,7 @@ public class DeviceAdminReceiver extends BroadcastReceiver { */ public void onEnabled(Context context, Intent intent) { } - + /** * Called when the user has asked to disable the administrator, as a result of * receiving {@link #ACTION_DEVICE_ADMIN_DISABLE_REQUESTED}, giving you @@ -224,7 +240,7 @@ public class DeviceAdminReceiver extends BroadcastReceiver { public CharSequence onDisableRequested(Context context, Intent intent) { return null; } - + /** * Called prior to the administrator being disabled, as a result of * receiving {@link #ACTION_DEVICE_ADMIN_DISABLED}. Upon return, you @@ -235,7 +251,7 @@ public class DeviceAdminReceiver extends BroadcastReceiver { */ public void onDisabled(Context context, Intent intent) { } - + /** * Called after the user has changed their password, as a result of * receiving {@link #ACTION_PASSWORD_CHANGED}. At this point you @@ -247,7 +263,7 @@ public class DeviceAdminReceiver extends BroadcastReceiver { */ public void onPasswordChanged(Context context, Intent intent) { } - + /** * Called after the user has failed at entering their current password, as a result of * receiving {@link #ACTION_PASSWORD_FAILED}. At this point you @@ -258,7 +274,7 @@ public class DeviceAdminReceiver extends BroadcastReceiver { */ public void onPasswordFailed(Context context, Intent intent) { } - + /** * Called after the user has succeeded at entering their current password, * as a result of receiving {@link #ACTION_PASSWORD_SUCCEEDED}. This will @@ -292,6 +308,26 @@ public class DeviceAdminReceiver extends BroadcastReceiver { } /** + * Called on the new profile when managed profile provisioning has completed. + * Managed profile provisioning is the process of setting up the device so that it has a + * separate profile which is managed by the mobile device management(mdm) application that + * triggered the provisioning. + * + * <p>As part of provisioning a new profile is created, the mdm is moved to the new profile and + * set as the owner of the profile so that it has full control over it. + * This intent is only received by the mdm package that is set as profile owner during + * provisioning. + * + * <p>Provisioning can be triggered via an intent with the action + * android.managedprovisioning.ACTION_PROVISION_MANAGED_PROFILE. + * + * @param context The running context as per {@link #onReceive}. + * @param intent The received intent as per {@link #onReceive}. + */ + public void onProfileProvisioningComplete(Context context, Intent intent) { + } + + /** * Intercept standard device administrator broadcasts. Implementations * should not override this method; it is better to implement the * convenience callbacks for each action. @@ -299,6 +335,7 @@ public class DeviceAdminReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); + if (ACTION_PASSWORD_CHANGED.equals(action)) { onPasswordChanged(context, intent); } else if (ACTION_PASSWORD_FAILED.equals(action)) { @@ -317,6 +354,8 @@ public class DeviceAdminReceiver extends BroadcastReceiver { onDisabled(context, intent); } else if (ACTION_PASSWORD_EXPIRING.equals(action)) { onPasswordExpiring(context, intent); + } else if (ACTION_PROFILE_PROVISIONING_COMPLETE.equals(action)) { + onProfileProvisioningComplete(context, intent); } } } diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index ab82531..725f808 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -22,10 +22,12 @@ import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.content.ComponentName; import android.content.Context; +import android.content.IntentFilter; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.Handler; +import android.os.Process; import android.os.RemoteCallback; import android.os.RemoteException; import android.os.ServiceManager; @@ -75,6 +77,43 @@ public class DevicePolicyManager { } /** + * Activity action: Starts the provisioning flow which sets up a managed profile. + * This intent will typically be sent by a mobile device management application(mdm). + * Managed profile provisioning creates a profile, moves the mdm to the profile, + * sets the mdm as the profile owner and removes all non required applications from the profile. + * As a profile owner the mdm than has full control over the managed profile. + * + * <p>The intent must contain the extras {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME} and + * {@link #EXTRA_PROVISIONING_DEFAULT_MANAGED_PROFILE_NAME}. + * + * <p> When managed provisioning has completed, an intent of the type + * {@link DeviceAdminReceiver#ACTION_PROFILE_PROVISIONING_COMPLETE} is broadcasted to the + * mdm app on the managed profile. + * + * <p>Input: Nothing.</p> + * <p>Output: Nothing</p> + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_PROVISION_MANAGED_PROFILE + = "android.app.action.ACTION_PROVISION_MANAGED_PROFILE"; + + /** + * A String extra holding the name of the package of the mobile device management application + * that starts the managed provisioning flow. This package will be set as the profile owner. + * <p>Use with {@link #ACTION_PROVISION_MANAGED_PROFILE}. + */ + public static final String EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME + = "deviceAdminPackageName"; + + /** + * A String extra holding the default name of the profile that is created during managed profile + * provisioning. + * <p>Use with {@link #ACTION_PROVISION_MANAGED_PROFILE} + */ + public static final String EXTRA_PROVISIONING_DEFAULT_MANAGED_PROFILE_NAME + = "defaultManagedProfileName"; + + /** * Activity action: ask the user to add a new device administrator to the system. * The desired policy is the ComponentName of the policy in the * {@link #EXTRA_DEVICE_ADMIN} extra field. This will invoke a UI to @@ -1153,7 +1192,9 @@ public class DevicePolicyManager { } exclSpec = listBuilder.toString(); } - android.net.Proxy.validate(hostName, Integer.toString(port), exclSpec); + if (android.net.Proxy.validate(hostName, Integer.toString(port), exclSpec) + != android.net.Proxy.PROXY_VALID) + throw new IllegalArgumentException(); } return mService.setGlobalProxy(admin, hostSpec, exclSpec, UserHandle.myUserId()); } catch (RemoteException e) { @@ -1493,10 +1534,10 @@ public class DevicePolicyManager { /** * @hide */ - public void setActiveAdmin(ComponentName policyReceiver, boolean refreshing) { + public void setActiveAdmin(ComponentName policyReceiver, boolean refreshing, int userHandle) { if (mService != null) { try { - mService.setActiveAdmin(policyReceiver, refreshing, UserHandle.myUserId()); + mService.setActiveAdmin(policyReceiver, refreshing, userHandle); } catch (RemoteException e) { Log.w(TAG, "Failed talking with device policy service", e); } @@ -1504,6 +1545,13 @@ public class DevicePolicyManager { } /** + * @hide + */ + public void setActiveAdmin(ComponentName policyReceiver, boolean refreshing) { + setActiveAdmin(policyReceiver, refreshing, UserHandle.myUserId()); + } + + /** * Returns the DeviceAdminInfo as defined by the administrator's package info & meta-data * @hide */ @@ -1681,4 +1729,155 @@ public class DevicePolicyManager { } return null; } + + /** + * @hide + * Sets the given package as the profile owner of the given user profile. The package must + * already be installed and there shouldn't be an existing profile owner registered for this + * user. Also, this method must be called before the user has been used for the first time. + * @param packageName the package name of the application to be registered as profile owner. + * @param ownerName the human readable name of the organisation associated with this DPM. + * @param userHandle the userId to set the profile owner for. + * @return whether the package was successfully registered as the profile owner. + * @throws IllegalArgumentException if packageName is null, the package isn't installed, or + * the user has already been set up. + */ + public boolean setProfileOwner(String packageName, String ownerName, int userHandle) + throws IllegalArgumentException { + if (mService != null) { + try { + return mService.setProfileOwner(packageName, ownerName, userHandle); + } catch (RemoteException re) { + Log.w(TAG, "Failed to set profile owner", re); + throw new IllegalArgumentException("Couldn't set profile owner.", re); + } + } + return false; + } + + /** + * Sets the enabled state of the profile. A profile should be enabled only once it is ready to + * be used. Only the profile owner can call this. + * + * @see #isPRofileOwnerApp + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + */ + public void setProfileEnabled(ComponentName admin) { + if (mService != null) { + try { + mService.setProfileEnabled(admin); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + } + + /** + * Used to determine if a particular package is registered as the Profile Owner for the + * current user. A profile owner is a special device admin that has additional privileges + * within the managed profile. + * + * @param packageName The package name of the app to compare with the registered profile owner. + * @return Whether or not the package is registered as the profile owner. + */ + public boolean isProfileOwnerApp(String packageName) { + if (mService != null) { + try { + String profileOwnerPackage = mService.getProfileOwner( + Process.myUserHandle().getIdentifier()); + return profileOwnerPackage != null && profileOwnerPackage.equals(packageName); + } catch (RemoteException re) { + Log.w(TAG, "Failed to check profile owner"); + } + } + return false; + } + + /** + * @hide + * @return the packageName of the owner of the given user profile or null if no profile + * owner has been set for that user. + * @throws IllegalArgumentException if the userId is invalid. + */ + public String getProfileOwner() throws IllegalArgumentException { + if (mService != null) { + try { + return mService.getProfileOwner(Process.myUserHandle().getIdentifier()); + } catch (RemoteException re) { + Log.w(TAG, "Failed to get profile owner"); + throw new IllegalArgumentException( + "Requested profile owner for invalid userId", re); + } + } + return null; + } + + /** + * @hide + * @return the human readable name of the organisation associated with this DPM or null if + * one is not set. + * @throws IllegalArgumentException if the userId is invalid. + */ + public String getProfileOwnerName() throws IllegalArgumentException { + if (mService != null) { + try { + return mService.getProfileOwnerName(Process.myUserHandle().getIdentifier()); + } catch (RemoteException re) { + Log.w(TAG, "Failed to get profile owner"); + throw new IllegalArgumentException( + "Requested profile owner for invalid userId", re); + } + } + return null; + } + + /** + * Called by a profile owner or device owner to add a default intent handler activity for + * intents that match a certain intent filter. This activity will remain the default intent + * handler even if the set of potential event handlers for the intent filter changes and if + * the intent preferences are reset. + * + * <p>The default disambiguation mechanism takes over if the activity is not installed + * (anymore). When the activity is (re)installed, it is automatically reset as default + * intent handler for the filter. + * + * <p>The calling device admin must be a profile owner or device owner. If it is not, a + * security exception will be thrown. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * @param filter The IntentFilter for which a default handler is added. + * @param activity The Activity that is added as default intent handler. + */ + public void addPersistentPreferredActivity(ComponentName admin, IntentFilter filter, + ComponentName activity) { + if (mService != null) { + try { + mService.addPersistentPreferredActivity(admin, filter, activity); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + } + + /** + * Called by a profile owner or device owner to remove all persistent intent handler preferences + * associated with the given package that were set by {@link #addPersistentPreferredActivity}. + * + * <p>The calling device admin must be a profile owner. If it is not, a security + * exception will be thrown. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * @param packageName The name of the package for which preferences are removed. + */ + public void clearPackagePersistentPreferredActivities(ComponentName admin, + String packageName) { + if (mService != null) { + try { + mService.clearPackagePersistentPreferredActivities(admin, packageName); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + } } diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index 172c47c..e4b2adc 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -18,6 +18,7 @@ package android.app.admin; import android.content.ComponentName; +import android.content.IntentFilter; import android.os.RemoteCallback; /** @@ -103,6 +104,14 @@ interface IDevicePolicyManager { String getDeviceOwner(); String getDeviceOwnerName(); + boolean setProfileOwner(String packageName, String ownerName, int userHandle); + String getProfileOwner(int userHandle); + String getProfileOwnerName(int userHandle); + void setProfileEnabled(in ComponentName who); + boolean installCaCert(in byte[] certBuffer); void uninstallCaCert(in byte[] certBuffer); + + void addPersistentPreferredActivity(in ComponentName admin, in IntentFilter filter, in ComponentName activity); + void clearPackagePersistentPreferredActivities(in ComponentName admin, String packageName); } diff --git a/core/java/android/app/backup/BackupAgent.java b/core/java/android/app/backup/BackupAgent.java index 67c772b..3c31f8d 100644 --- a/core/java/android/app/backup/BackupAgent.java +++ b/core/java/android/app/backup/BackupAgent.java @@ -128,6 +128,13 @@ public abstract class BackupAgent extends ContextWrapper { Handler mHandler = null; + Handler getHandler() { + if (mHandler == null) { + mHandler = new Handler(Looper.getMainLooper()); + } + return mHandler; + } + class SharedPrefsSynchronizer implements Runnable { public final CountDownLatch mLatch = new CountDownLatch(1); @@ -140,12 +147,9 @@ public abstract class BackupAgent extends ContextWrapper { // Syncing shared preferences deferred writes needs to happen on the main looper thread private void waitForSharedPrefs() { - if (mHandler == null) { - mHandler = new Handler(Looper.getMainLooper()); - } - + Handler h = getHandler(); final SharedPrefsSynchronizer s = new SharedPrefsSynchronizer(); - mHandler.postAtFrontOfQueue(s); + h.postAtFrontOfQueue(s); try { s.mLatch.await(); } catch (InterruptedException e) { /* ignored */ } @@ -680,5 +684,23 @@ public abstract class BackupAgent extends ContextWrapper { } } } + + @Override + public void fail(String message) { + getHandler().post(new FailRunnable(message)); + } + } + + static class FailRunnable implements Runnable { + private String mMessage; + + FailRunnable(String message) { + mMessage = message; + } + + @Override + public void run() { + throw new IllegalStateException(mMessage); + } } } diff --git a/core/java/android/app/backup/BackupDataOutput.java b/core/java/android/app/backup/BackupDataOutput.java index 3a070b6..fc5fb3d 100644 --- a/core/java/android/app/backup/BackupDataOutput.java +++ b/core/java/android/app/backup/BackupDataOutput.java @@ -17,6 +17,7 @@ package android.app.backup; import android.os.ParcelFileDescriptor; +import android.os.Process; import java.io.FileDescriptor; import java.io.IOException; @@ -76,7 +77,8 @@ public class BackupDataOutput { /** * Mark the beginning of one record in the backup data stream. This must be called before * {@link #writeEntityData}. - * @param key A string key that uniquely identifies the data record within the application + * @param key A string key that uniquely identifies the data record within the application. + * Keys whose first character is \uFF00 or higher are not valid. * @param dataSize The size in bytes of this record's data. Passing a dataSize * of -1 indicates that the record under this key should be deleted. * @return The number of bytes written to the backup stream diff --git a/core/java/android/app/backup/FullBackup.java b/core/java/android/app/backup/FullBackup.java index cb0737e..cfd0a65 100644 --- a/core/java/android/app/backup/FullBackup.java +++ b/core/java/android/app/backup/FullBackup.java @@ -16,9 +16,6 @@ package android.app.backup; -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; import android.os.ParcelFileDescriptor; import android.util.Log; diff --git a/core/java/android/app/backup/IBackupManager.aidl b/core/java/android/app/backup/IBackupManager.aidl index 12ee3b6..c629a2e 100644 --- a/core/java/android/app/backup/IBackupManager.aidl +++ b/core/java/android/app/backup/IBackupManager.aidl @@ -167,7 +167,7 @@ interface IBackupManager { * are to be backed up. The <code>allApps</code> parameter supersedes this. */ void fullBackup(in ParcelFileDescriptor fd, boolean includeApks, boolean includeObbs, - boolean includeShared, boolean allApps, boolean allIncludesSystem, + boolean includeShared, boolean doWidgets, boolean allApps, boolean allIncludesSystem, in String[] packageNames); /** diff --git a/core/java/android/app/backup/SharedPreferencesBackupHelper.java b/core/java/android/app/backup/SharedPreferencesBackupHelper.java index 213bd31..939616b 100644 --- a/core/java/android/app/backup/SharedPreferencesBackupHelper.java +++ b/core/java/android/app/backup/SharedPreferencesBackupHelper.java @@ -18,7 +18,6 @@ package android.app.backup; import android.app.QueuedWork; import android.content.Context; -import android.content.SharedPreferences; import android.os.ParcelFileDescriptor; import android.util.Log; diff --git a/core/java/android/app/maintenance/IIdleCallback.aidl b/core/java/android/app/maintenance/IIdleCallback.aidl new file mode 100644 index 0000000..582dede --- /dev/null +++ b/core/java/android/app/maintenance/IIdleCallback.aidl @@ -0,0 +1,53 @@ +/** + * Copyright 2014, 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.app.maintenance; + +import android.app.maintenance.IIdleService; + +/** + * The server side of the idle maintenance IPC protocols. The app-side implementation + * invokes on this interface to indicate completion of the (asynchronous) instructions + * issued by the server. + * + * In all cases, the 'who' parameter is the caller's service binder, used to track + * which idle service instance is reporting. + * + * {@hide} + */ +interface IIdleCallback { + /** + * Acknowledge receipt and processing of the asynchronous "start idle work" incall. + * 'result' is true if the app wants some time to perform ongoing background + * idle-time work; or false if the app declares that it does not need any time + * for such work. + */ + void acknowledgeStart(int token, boolean result); + + /** + * Acknowledge receipt and processing of the asynchronous "stop idle work" incall. + */ + void acknowledgeStop(int token); + + /* + * Tell the idle service manager that we're done with our idle maintenance, so that + * it can go on to the next one and stop attributing wakelock time to us etc. + * + * @param opToken The identifier passed in the startIdleMaintenance() call that + * indicated the beginning of this service's idle timeslice. + */ + void idleFinished(int token); +} diff --git a/core/java/android/app/maintenance/IIdleService.aidl b/core/java/android/app/maintenance/IIdleService.aidl new file mode 100644 index 0000000..54abccd --- /dev/null +++ b/core/java/android/app/maintenance/IIdleService.aidl @@ -0,0 +1,34 @@ +/** + * Copyright 2014, 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.app.maintenance; + +import android.app.maintenance.IIdleCallback; + +/** + * Interface that the framework uses to communicate with application code + * that implements an idle-time "maintenance" service. End user code does + * not implement this interface directly; instead, the app's idle service + * implementation will extend android.app.maintenance.IdleService. + * {@hide} + */ +oneway interface IIdleService { + /** + * Begin your idle-time work. + */ + void startIdleMaintenance(IIdleCallback callbackBinder, int token); + void stopIdleMaintenance(IIdleCallback callbackBinder, int token); +} diff --git a/core/java/android/app/maintenance/IdleService.java b/core/java/android/app/maintenance/IdleService.java new file mode 100644 index 0000000..2331b81 --- /dev/null +++ b/core/java/android/app/maintenance/IdleService.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2014 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.app.maintenance; + +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; +import android.app.Service; +import android.content.Intent; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.util.Log; +import android.util.Slog; + +/** + * Idle maintenance API. Full docs TBW (to be written). + */ +public abstract class IdleService extends Service { + private static final String TAG = "IdleService"; + + static final int MSG_START = 1; + static final int MSG_STOP = 2; + static final int MSG_FINISH = 3; + + IdleHandler mHandler; + IIdleCallback mCallbackBinder; + int mToken; + final Object mHandlerLock = new Object(); + + void ensureHandler() { + synchronized (mHandlerLock) { + if (mHandler == null) { + mHandler = new IdleHandler(getMainLooper()); + } + } + } + + /** + * TBW: the idle service should supply an intent-filter handling this intent + * <p> + * <p class="note">The application must also protect the idle service with the + * {@code "android.permission.BIND_IDLE_SERVICE"} permission to ensure that other + * applications cannot maliciously bind to it. If an idle service's manifest + * declaration does not require that permission, it will never be invoked. + * </p> + */ + @SdkConstant(SdkConstantType.SERVICE_ACTION) + public static final String SERVICE_INTERFACE = + "android.service.idle.IdleService"; + + /** + * Idle services must be protected with this permission: + * + * <pre class="prettyprint"> + * <service android:name="MyIdleService" + * android:permission="android.permission.BIND_IDLE_SERVICE" > + * ... + * </service> + * </pre> + * + * <p>If an idle service is declared in the manifest but not protected with this + * permission, that service will be ignored by the OS. + */ + public static final String PERMISSION_BIND = + "android.permission.BIND_IDLE_SERVICE"; + + // Trampoline: the callbacks are always run on the main thread + IIdleService mBinder = new IIdleService.Stub() { + @Override + public void startIdleMaintenance(IIdleCallback callbackBinder, int token) + throws RemoteException { + ensureHandler(); + Message msg = mHandler.obtainMessage(MSG_START, token, 0, callbackBinder); + mHandler.sendMessage(msg); + } + + @Override + public void stopIdleMaintenance(IIdleCallback callbackBinder, int token) + throws RemoteException { + ensureHandler(); + Message msg = mHandler.obtainMessage(MSG_STOP, token, 0, callbackBinder); + mHandler.sendMessage(msg); + } + }; + + /** + * Your application may begin doing "idle" maintenance work in the background. + * <p> + * Your application may continue to run in the background until it receives a call + * to {@link #onIdleStop()}, at which point you <i>must</i> cease doing work. The + * OS will hold a wakelock on your application's behalf from the time this method is + * called until after the following call to {@link #onIdleStop()} returns. + * </p> + * <p> + * Returning {@code false} from this method indicates that you have no ongoing work + * to do at present. The OS will respond by immediately calling {@link #onIdleStop()} + * and returning your application to its normal stopped state. Returning {@code true} + * indicates that the application is indeed performing ongoing work, so the OS will + * let your application run in this state until it's no longer appropriate. + * </p> + * <p> + * You will always receive a matching call to {@link #onIdleStop()} even if your + * application returns {@code false} from this method. + * + * @return {@code true} to indicate that the application wishes to perform some ongoing + * background work; {@code false} to indicate that it does not need to perform such + * work at present. + */ + public abstract boolean onIdleStart(); + + /** + * Your app's maintenance opportunity is over. Once the application returns from + * this method, the wakelock held by the OS on its behalf will be released. + */ + public abstract void onIdleStop(); + + /** + * Tell the OS that you have finished your idle work. Calling this more than once, + * or calling it when you have not received an {@link #onIdleStart()} callback, is + * an error. + * + * <p>It is safe to call {@link #finishIdle()} from any thread. + */ + public final void finishIdle() { + ensureHandler(); + mHandler.sendEmptyMessage(MSG_FINISH); + } + + class IdleHandler extends Handler { + IdleHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_START: { + // Call the concrete onIdleStart(), reporting its return value back to + // the OS. If onIdleStart() throws, report it as a 'false' return but + // rethrow the exception at the offending app. + boolean result = false; + IIdleCallback callbackBinder = (IIdleCallback) msg.obj; + mCallbackBinder = callbackBinder; + final int token = mToken = msg.arg1; + try { + result = IdleService.this.onIdleStart(); + } catch (Exception e) { + Log.e(TAG, "Unable to start idle workload", e); + throw new RuntimeException(e); + } finally { + // don't bother if the service already called finishIdle() + if (mCallbackBinder != null) { + try { + callbackBinder.acknowledgeStart(token, result); + } catch (RemoteException re) { + Log.e(TAG, "System unreachable to start idle workload"); + } + } + } + break; + } + + case MSG_STOP: { + // Structured just like MSG_START for the stop-idle bookend call. + IIdleCallback callbackBinder = (IIdleCallback) msg.obj; + final int token = msg.arg1; + try { + IdleService.this.onIdleStop(); + } catch (Exception e) { + Log.e(TAG, "Unable to stop idle workload", e); + throw new RuntimeException(e); + } finally { + if (mCallbackBinder != null) { + try { + callbackBinder.acknowledgeStop(token); + } catch (RemoteException re) { + Log.e(TAG, "System unreachable to stop idle workload"); + } + } + } + break; + } + + case MSG_FINISH: { + if (mCallbackBinder != null) { + try { + mCallbackBinder.idleFinished(mToken); + } catch (RemoteException e) { + Log.e(TAG, "System unreachable to finish idling"); + } finally { + mCallbackBinder = null; + } + } else { + Log.e(TAG, "finishIdle() called but the idle service is not started"); + } + break; + } + + default: { + Slog.w(TAG, "Unknown message " + msg.what); + } + } + } + } + + /** @hide */ + @Override + public final IBinder onBind(Intent intent) { + return mBinder.asBinder(); + } + +} diff --git a/core/java/android/view/IMagnificationCallbacks.aidl b/core/java/android/app/trust/ITrustListener.aidl index 032d073..4680043 100644 --- a/core/java/android/view/IMagnificationCallbacks.aidl +++ b/core/java/android/app/trust/ITrustListener.aidl @@ -1,5 +1,6 @@ /* -** Copyright 2012, The Android Open Source Project +** +** Copyright 2014, 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. @@ -13,17 +14,13 @@ ** See the License for the specific language governing permissions and ** limitations under the License. */ - -package android.view; - -import android.graphics.Region; +package android.app.trust; /** + * Private API to be notified about trust changes. + * * {@hide} */ -oneway interface IMagnificationCallbacks { - void onMagnifedBoundsChanged(in Region bounds); - void onRectangleOnScreenRequested(int left, int top, int right, int bottom); - void onRotationChanged(int rotation); - void onUserContextChanged(); -} +oneway interface ITrustListener { + void onTrustChanged(boolean enabled, int userId); +}
\ No newline at end of file diff --git a/core/java/android/app/trust/ITrustManager.aidl b/core/java/android/app/trust/ITrustManager.aidl new file mode 100644 index 0000000..ad4ccbb --- /dev/null +++ b/core/java/android/app/trust/ITrustManager.aidl @@ -0,0 +1,31 @@ +/* +** +** Copyright 2014, 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.app.trust; + +import android.app.trust.ITrustListener; + +/** + * System private API to comunicate with trust service. + * + * {@hide} + */ +interface ITrustManager { + void reportUnlockAttempt(boolean successful, int userId); + void reportEnabledTrustAgentsChanged(int userId); + void registerTrustListener(in ITrustListener trustListener); + void unregisterTrustListener(in ITrustListener trustListener); +} diff --git a/core/java/android/app/trust/TrustManager.java b/core/java/android/app/trust/TrustManager.java new file mode 100644 index 0000000..e31c624 --- /dev/null +++ b/core/java/android/app/trust/TrustManager.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2014 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.app.trust; + +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.util.ArrayMap; +import android.util.Log; + +/** + * See {@link com.android.server.trust.TrustManagerService} + * @hide + */ +public class TrustManager { + + private static final int MSG_TRUST_CHANGED = 1; + + private static final String TAG = "TrustManager"; + + private final ITrustManager mService; + private final ArrayMap<TrustListener, ITrustListener> mTrustListeners; + + public TrustManager(IBinder b) { + mService = ITrustManager.Stub.asInterface(b); + mTrustListeners = new ArrayMap<TrustListener, ITrustListener>(); + } + + /** + * Reports that user {@param userId} has tried to unlock the device. + * + * @param successful if true, the unlock attempt was successful. + * + * Requires the {@link android.Manifest.permission#ACCESS_KEYGUARD_SECURE_STORAGE} permission. + */ + public void reportUnlockAttempt(boolean successful, int userId) { + try { + mService.reportUnlockAttempt(successful, userId); + } catch (RemoteException e) { + onError(e); + } + } + + /** + * Reports that the list of enabled trust agents changed for user {@param userId}. + * + * Requires the {@link android.Manifest.permission#ACCESS_KEYGUARD_SECURE_STORAGE} permission. + */ + public void reportEnabledTrustAgentsChanged(int userId) { + try { + mService.reportEnabledTrustAgentsChanged(userId); + } catch (RemoteException e) { + onError(e); + } + } + + /** + * Registers a listener for trust events. + * + * Requires the {@link android.Manifest.permission#TRUST_LISTENER} permission. + */ + public void registerTrustListener(final TrustListener trustListener) { + try { + ITrustListener.Stub iTrustListener = new ITrustListener.Stub() { + @Override + public void onTrustChanged(boolean enabled, int userId) throws RemoteException { + mHandler.obtainMessage(MSG_TRUST_CHANGED, (enabled ? 1 : 0), userId, + trustListener).sendToTarget(); + } + }; + mService.registerTrustListener(iTrustListener); + mTrustListeners.put(trustListener, iTrustListener); + } catch (RemoteException e) { + onError(e); + } + } + + /** + * Unregisters a listener for trust events. + * + * Requires the {@link android.Manifest.permission#TRUST_LISTENER} permission. + */ + public void unregisterTrustListener(final TrustListener trustListener) { + ITrustListener iTrustListener = mTrustListeners.remove(trustListener); + if (iTrustListener != null) { + try { + mService.unregisterTrustListener(iTrustListener); + } catch (RemoteException e) { + onError(e); + } + } + } + + private void onError(Exception e) { + Log.e(TAG, "Error while calling TrustManagerService", e); + } + + private final Handler mHandler = new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(Message msg) { + switch(msg.what) { + case MSG_TRUST_CHANGED: + ((TrustListener)msg.obj).onTrustChanged(msg.arg1 != 0, msg.arg2); + break; + } + } + }; + + public interface TrustListener { + + /** + * Reports that the trust state has changed. + * @param enabled if true, the system believes the environment to be trusted. + * @param userId the user, for which the trust changed. + */ + void onTrustChanged(boolean enabled, int userId); + } +} diff --git a/core/java/android/appwidget/AppWidgetHost.java b/core/java/android/appwidget/AppWidgetHost.java index f104d71..84d3835 100644 --- a/core/java/android/appwidget/AppWidgetHost.java +++ b/core/java/android/appwidget/AppWidgetHost.java @@ -19,7 +19,6 @@ package android.appwidget; import java.util.ArrayList; import java.util.HashMap; -import android.app.ActivityThread; import android.content.Context; import android.os.Binder; import android.os.Handler; @@ -31,7 +30,6 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; import android.util.DisplayMetrics; -import android.util.Log; import android.util.TypedValue; import android.widget.RemoteViews; import android.widget.RemoteViews.OnClickHandler; diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java index d1c7bec..dd3a871 100644 --- a/core/java/android/appwidget/AppWidgetManager.java +++ b/core/java/android/appwidget/AppWidgetManager.java @@ -16,7 +16,6 @@ package android.appwidget; -import android.app.ActivityManagerNative; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -215,6 +214,12 @@ public class AppWidgetManager { public static final String EXTRA_CUSTOM_INFO = "customInfo"; /** + * An intent extra attached to the {@link #ACTION_APPWIDGET_HOST_RESTORED} broadcast, + * indicating the integer ID of the host whose widgets have just been restored. + */ + public static final String EXTRA_HOST_ID = "hostId"; + + /** * An intent extra to pass to the AppWidget picker containing a {@link java.util.List} of * {@link android.os.Bundle} objects to mix in to the list of AppWidgets that are * installed. It will be added to the extras object on the {@link android.content.Intent} @@ -311,6 +316,86 @@ public class AppWidgetManager { public static final String ACTION_APPWIDGET_ENABLED = "android.appwidget.action.APPWIDGET_ENABLED"; /** + * Sent to providers after AppWidget state related to the provider has been restored from + * backup. The intent contains information about how to translate AppWidget ids from the + * restored data to their new equivalents. + * + * <p>The intent will contain the following extras: + * + * <table> + * <tr> + * <td>{@link #EXTRA_APPWIDGET_OLD_IDS}</td> + * <td>The set of appWidgetIds represented in a restored backup that have been successfully + * incorporated into the current environment. This may be all of the AppWidgets known + * to this application, or just a subset. Each entry in this array of appWidgetIds has + * a corresponding entry in the {@link #EXTRA_APPWIDGET_IDS} extra.</td> + * </tr> + * <tr> + * <td>{@link #EXTRA_APPWIDGET_IDS}</td> + * <td>The set of appWidgetIds now valid for this application. The app should look at + * its restored widget configuration and translate each appWidgetId in the + * {@link #EXTRA_APPWIDGET_OLD_IDS} array to its new value found at the corresponding + * index within this array.</td> + * </tr> + * </table> + * + * <p class="note">This is a protected intent that can only be sent + * by the system. + * + * @see {@link #ACTION_APPWIDGET_HOST_RESTORED} for the corresponding host broadcast + */ + public static final String ACTION_APPWIDGET_RESTORED + = "android.appwidget.action.APPWIDGET_RESTORED"; + + /** + * Sent to widget hosts after AppWidget state related to the host has been restored from + * backup. The intent contains information about how to translate AppWidget ids from the + * restored data to their new equivalents. If an application maintains multiple separate + * widget hosts instances, it will receive this broadcast separately for each one. + * + * <p>The intent will contain the following extras: + * + * <table> + * <tr> + * <td>{@link #EXTRA_APPWIDGET_OLD_IDS}</td> + * <td>The set of appWidgetIds represented in a restored backup that have been successfully + * incorporated into the current environment. This may be all of the AppWidgets known + * to this application, or just a subset. Each entry in this array of appWidgetIds has + * a corresponding entry in the {@link #EXTRA_APPWIDGET_IDS} extra.</td> + * </tr> + * <tr> + * <td>{@link #EXTRA_APPWIDGET_IDS}</td> + * <td>The set of appWidgetIds now valid for this application. The app should look at + * its restored widget configuration and translate each appWidgetId in the + * {@link #EXTRA_APPWIDGET_OLD_IDS} array to its new value found at the corresponding + * index within this array.</td> + * </tr> + * <tr> + * <td>{@link #EXTRA_HOST_ID}</td> + * <td>The integer ID of the widget host instance whose state has just been restored.</td> + * </tr> + * </table> + * + * <p class="note">This is a protected intent that can only be sent + * by the system. + * + * @see {@link #ACTION_APPWIDGET_RESTORED} for the corresponding provider broadcast + */ + public static final String ACTION_APPWIDGET_HOST_RESTORED + = "android.appwidget.action.APPWIDGET_HOST_RESTORED"; + + /** + * An intent extra that contains multiple appWidgetIds. These are id values as + * they were provided to the application during a recent restore from backup. It is + * attached to the {@link #ACTION_APPWIDGET_RESTORED} broadcast intent. + * + * <p> + * The value will be an int array that can be retrieved like this: + * {@sample frameworks/base/tests/appwidgets/AppWidgetHostTest/src/com/android/tests/appwidgethost/TestAppWidgetProvider.java getExtra_EXTRA_APPWIDGET_IDS} + */ + public static final String EXTRA_APPWIDGET_OLD_IDS = "appWidgetOldIds"; + + /** * Field for the manifest meta-data tag. * * @see AppWidgetProviderInfo diff --git a/core/java/android/appwidget/AppWidgetProvider.java b/core/java/android/appwidget/AppWidgetProvider.java index edf142b..ab91edf 100644 --- a/core/java/android/appwidget/AppWidgetProvider.java +++ b/core/java/android/appwidget/AppWidgetProvider.java @@ -66,15 +66,13 @@ public class AppWidgetProvider extends BroadcastReceiver { this.onUpdate(context, AppWidgetManager.getInstance(context), appWidgetIds); } } - } - else if (AppWidgetManager.ACTION_APPWIDGET_DELETED.equals(action)) { + } else if (AppWidgetManager.ACTION_APPWIDGET_DELETED.equals(action)) { Bundle extras = intent.getExtras(); if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID)) { final int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID); this.onDeleted(context, new int[] { appWidgetId }); } - } - else if (AppWidgetManager.ACTION_APPWIDGET_OPTIONS_CHANGED.equals(action)) { + } else if (AppWidgetManager.ACTION_APPWIDGET_OPTIONS_CHANGED.equals(action)) { Bundle extras = intent.getExtras(); if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID) && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS)) { @@ -83,19 +81,28 @@ public class AppWidgetProvider extends BroadcastReceiver { this.onAppWidgetOptionsChanged(context, AppWidgetManager.getInstance(context), appWidgetId, widgetExtras); } - } - else if (AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)) { + } else if (AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)) { this.onEnabled(context); - } - else if (AppWidgetManager.ACTION_APPWIDGET_DISABLED.equals(action)) { + } else if (AppWidgetManager.ACTION_APPWIDGET_DISABLED.equals(action)) { this.onDisabled(context); + } else if (AppWidgetManager.ACTION_APPWIDGET_RESTORED.equals(action)) { + Bundle extras = intent.getExtras(); + if (extras != null) { + int[] oldIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_OLD_IDS); + int[] newIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS); + if (oldIds != null && oldIds.length > 0) { + this.onRestored(context, oldIds, newIds); + this.onUpdate(context, AppWidgetManager.getInstance(context), newIds); + } + } } } // END_INCLUDE(onReceive) /** - * Called in response to the {@link AppWidgetManager#ACTION_APPWIDGET_UPDATE} broadcast when - * this AppWidget provider is being asked to provide {@link android.widget.RemoteViews RemoteViews} + * Called in response to the {@link AppWidgetManager#ACTION_APPWIDGET_UPDATE} and + * {@link AppWidgetManager#ACTION_APPWIDGET_RESTORED} broadcasts when this AppWidget + * provider is being asked to provide {@link android.widget.RemoteViews RemoteViews} * for a set of AppWidgets. Override this method to implement your own AppWidget functionality. * * {@more} @@ -123,8 +130,8 @@ public class AppWidgetProvider extends BroadcastReceiver { * running. * @param appWidgetManager A {@link AppWidgetManager} object you can call {@link * AppWidgetManager#updateAppWidget} on. - * @param appWidgetId The appWidgetId of the widget who's size changed. - * @param newOptions The appWidgetId of the widget who's size changed. + * @param appWidgetId The appWidgetId of the widget whose size changed. + * @param newOptions The appWidgetId of the widget whose size changed. * * @see AppWidgetManager#ACTION_APPWIDGET_OPTIONS_CHANGED */ @@ -181,4 +188,24 @@ public class AppWidgetProvider extends BroadcastReceiver { */ public void onDisabled(Context context) { } + + /** + * Called in response to the {@link AppWidgetManager#ACTION_APPWIDGET_RESTORED} broadcast + * when instances of this AppWidget provider have been restored from backup. If your + * provider maintains any persistent data about its widget instances, override this method + * to remap the old AppWidgetIds to the new values and update any other app state that may + * be relevant. + * + * <p>This callback will be followed immediately by a call to {@link #onUpdate} so your + * provider can immediately generate new RemoteViews suitable for its newly-restored set + * of instances. + * + * {@more} + * + * @param context + * @param oldWidgetIds + * @param newWidgetIds + */ + public void onRestored(Context context, int[] oldWidgetIds, int[] newWidgetIds) { + } } diff --git a/core/java/android/appwidget/AppWidgetProviderInfo.java b/core/java/android/appwidget/AppWidgetProviderInfo.java index 7b8b286..4b33799 100644 --- a/core/java/android/appwidget/AppWidgetProviderInfo.java +++ b/core/java/android/appwidget/AppWidgetProviderInfo.java @@ -172,7 +172,7 @@ public class AppWidgetProviderInfo implements Parcelable { * <p>This field corresponds to the <code>android:previewImage</code> attribute in * the <code><receiver></code> element in the AndroidManifest.xml file. */ - public int previewImage; + public int previewImage; /** * The rules by which a widget can be resized. See {@link #RESIZE_NONE}, diff --git a/core/java/android/bluetooth/BluetoothA2dp.java b/core/java/android/bluetooth/BluetoothA2dp.java index 6f929f2..7b709ac 100644 --- a/core/java/android/bluetooth/BluetoothA2dp.java +++ b/core/java/android/bluetooth/BluetoothA2dp.java @@ -162,7 +162,8 @@ public final class BluetoothA2dp implements BluetoothProfile { Intent intent = new Intent(IBluetoothA2dp.class.getName()); ComponentName comp = intent.resolveSystemService(mContext.getPackageManager(), 0); intent.setComponent(comp); - if (comp == null || !mContext.bindService(intent, mConnection, 0)) { + if (comp == null || !mContext.bindServiceAsUser(intent, mConnection, 0, + android.os.Process.myUserHandle())) { Log.e(TAG, "Could not bind to Bluetooth A2DP Service with " + intent); return false; } diff --git a/core/java/android/bluetooth/BluetoothAdapter.java b/core/java/android/bluetooth/BluetoothAdapter.java index 646be06..e79deec 100644 --- a/core/java/android/bluetooth/BluetoothAdapter.java +++ b/core/java/android/bluetooth/BluetoothAdapter.java @@ -19,11 +19,9 @@ package android.bluetooth; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.content.Context; -import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.Looper; -import android.os.Message; import android.os.ParcelUuid; import android.os.RemoteException; import android.os.ServiceManager; @@ -39,7 +37,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Locale; import java.util.Map; -import java.util.Random; import java.util.Set; import java.util.UUID; diff --git a/core/java/android/bluetooth/BluetoothDevice.java b/core/java/android/bluetooth/BluetoothDevice.java index d789a94..a396a05 100644 --- a/core/java/android/bluetooth/BluetoothDevice.java +++ b/core/java/android/bluetooth/BluetoothDevice.java @@ -19,12 +19,10 @@ package android.bluetooth; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.content.Context; -import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; import android.os.ParcelUuid; import android.os.RemoteException; -import android.os.ServiceManager; import android.util.Log; import java.io.IOException; @@ -947,8 +945,13 @@ public final class BluetoothDevice implements Parcelable { * was started. */ public boolean fetchUuidsWithSdp() { + IBluetooth service = sService; + if (service == null) { + Log.e(TAG, "BT not enabled. Cannot fetchUuidsWithSdp"); + return false; + } try { - return sService.fetchRemoteUuids(this); + return service.fetchRemoteUuids(this); } catch (RemoteException e) {Log.e(TAG, "", e);} return false; } diff --git a/core/java/android/bluetooth/BluetoothGatt.java b/core/java/android/bluetooth/BluetoothGatt.java index e7ab8de..ff3af7c 100644 --- a/core/java/android/bluetooth/BluetoothGatt.java +++ b/core/java/android/bluetooth/BluetoothGatt.java @@ -16,20 +16,9 @@ package android.bluetooth; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothProfile; -import android.bluetooth.BluetoothProfile.ServiceListener; -import android.bluetooth.IBluetoothManager; -import android.bluetooth.IBluetoothStateChangeCallback; - -import android.content.ComponentName; import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.os.IBinder; import android.os.ParcelUuid; import android.os.RemoteException; -import android.os.ServiceManager; import android.util.Log; import java.util.ArrayList; diff --git a/core/java/android/bluetooth/BluetoothGattCharacteristic.java b/core/java/android/bluetooth/BluetoothGattCharacteristic.java index f0ecbb4..a86677c 100644 --- a/core/java/android/bluetooth/BluetoothGattCharacteristic.java +++ b/core/java/android/bluetooth/BluetoothGattCharacteristic.java @@ -16,7 +16,6 @@ package android.bluetooth; import java.util.ArrayList; -import java.util.IllegalFormatConversionException; import java.util.List; import java.util.UUID; diff --git a/core/java/android/bluetooth/BluetoothGattServer.java b/core/java/android/bluetooth/BluetoothGattServer.java index 153215c..0c00c06 100644 --- a/core/java/android/bluetooth/BluetoothGattServer.java +++ b/core/java/android/bluetooth/BluetoothGattServer.java @@ -19,18 +19,9 @@ package android.bluetooth; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; -import android.bluetooth.BluetoothProfile.ServiceListener; -import android.bluetooth.IBluetoothManager; -import android.bluetooth.IBluetoothStateChangeCallback; - -import android.content.ComponentName; import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.os.IBinder; import android.os.ParcelUuid; import android.os.RemoteException; -import android.os.ServiceManager; import android.util.Log; import java.util.ArrayList; diff --git a/core/java/android/bluetooth/BluetoothGattServerCallback.java b/core/java/android/bluetooth/BluetoothGattServerCallback.java index f9f1d97..fc3ffe8 100644 --- a/core/java/android/bluetooth/BluetoothGattServerCallback.java +++ b/core/java/android/bluetooth/BluetoothGattServerCallback.java @@ -18,8 +18,6 @@ package android.bluetooth; import android.bluetooth.BluetoothDevice; -import android.util.Log; - /** * This abstract class is used to implement {@link BluetoothGattServer} callbacks. */ diff --git a/core/java/android/bluetooth/BluetoothHeadset.java b/core/java/android/bluetooth/BluetoothHeadset.java index 8ee955d..f88a173 100644 --- a/core/java/android/bluetooth/BluetoothHeadset.java +++ b/core/java/android/bluetooth/BluetoothHeadset.java @@ -280,7 +280,8 @@ public final class BluetoothHeadset implements BluetoothProfile { Intent intent = new Intent(IBluetoothHeadset.class.getName()); ComponentName comp = intent.resolveSystemService(mContext.getPackageManager(), 0); intent.setComponent(comp); - if (comp == null || !mContext.bindService(intent, mConnection, 0)) { + if (comp == null || !mContext.bindServiceAsUser(intent, mConnection, 0, + android.os.Process.myUserHandle())) { Log.e(TAG, "Could not bind to Bluetooth Headset Service with " + intent); return false; } diff --git a/core/java/android/bluetooth/BluetoothHealth.java b/core/java/android/bluetooth/BluetoothHealth.java index 2e950fa..4949c24 100644 --- a/core/java/android/bluetooth/BluetoothHealth.java +++ b/core/java/android/bluetooth/BluetoothHealth.java @@ -23,7 +23,6 @@ import android.content.ServiceConnection; import android.os.IBinder; import android.os.ParcelFileDescriptor; import android.os.RemoteException; -import android.os.ServiceManager; import android.util.Log; import java.util.ArrayList; @@ -488,7 +487,8 @@ public final class BluetoothHealth implements BluetoothProfile { Intent intent = new Intent(IBluetoothHealth.class.getName()); ComponentName comp = intent.resolveSystemService(mContext.getPackageManager(), 0); intent.setComponent(comp); - if (comp == null || !mContext.bindService(intent, mConnection, 0)) { + if (comp == null || !mContext.bindServiceAsUser(intent, mConnection, 0, + android.os.Process.myUserHandle())) { Log.e(TAG, "Could not bind to Bluetooth Health Service with " + intent); return false; } diff --git a/core/java/android/bluetooth/BluetoothInputDevice.java b/core/java/android/bluetooth/BluetoothInputDevice.java index c48b15d..554df3e 100644 --- a/core/java/android/bluetooth/BluetoothInputDevice.java +++ b/core/java/android/bluetooth/BluetoothInputDevice.java @@ -24,7 +24,6 @@ import android.content.Intent; import android.content.ServiceConnection; import android.os.IBinder; import android.os.RemoteException; -import android.os.ServiceManager; import android.util.Log; import java.util.ArrayList; @@ -260,7 +259,8 @@ public final class BluetoothInputDevice implements BluetoothProfile { Intent intent = new Intent(IBluetoothInputDevice.class.getName()); ComponentName comp = intent.resolveSystemService(mContext.getPackageManager(), 0); intent.setComponent(comp); - if (comp == null || !mContext.bindService(intent, mConnection, 0)) { + if (comp == null || !mContext.bindServiceAsUser(intent, mConnection, 0, + android.os.Process.myUserHandle())) { Log.e(TAG, "Could not bind to Bluetooth HID Service with " + intent); return false; } diff --git a/core/java/android/bluetooth/BluetoothMap.java b/core/java/android/bluetooth/BluetoothMap.java index 92a2f1e..7f57acf 100644 --- a/core/java/android/bluetooth/BluetoothMap.java +++ b/core/java/android/bluetooth/BluetoothMap.java @@ -22,9 +22,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; -import android.os.RemoteException; -import android.os.IBinder; -import android.os.ServiceManager; +import android.os.*; import android.util.Log; /** @@ -106,7 +104,8 @@ public final class BluetoothMap implements BluetoothProfile { Intent intent = new Intent(IBluetoothMap.class.getName()); ComponentName comp = intent.resolveSystemService(mContext.getPackageManager(), 0); intent.setComponent(comp); - if (comp == null || !mContext.bindService(intent, mConnection, 0)) { + if (comp == null || !mContext.bindServiceAsUser(intent, mConnection, 0, + android.os.Process.myUserHandle())) { Log.e(TAG, "Could not bind to Bluetooth MAP Service with " + intent); return false; } diff --git a/core/java/android/bluetooth/BluetoothPan.java b/core/java/android/bluetooth/BluetoothPan.java index b7a37f4..4f81f98 100644 --- a/core/java/android/bluetooth/BluetoothPan.java +++ b/core/java/android/bluetooth/BluetoothPan.java @@ -24,7 +24,6 @@ import android.content.Intent; import android.content.ServiceConnection; import android.os.IBinder; import android.os.RemoteException; -import android.os.ServiceManager; import android.util.Log; import java.util.ArrayList; @@ -146,7 +145,8 @@ public final class BluetoothPan implements BluetoothProfile { Intent intent = new Intent(IBluetoothPan.class.getName()); ComponentName comp = intent.resolveSystemService(mContext.getPackageManager(), 0); intent.setComponent(comp); - if (comp == null || !mContext.bindService(intent, mConnection, 0)) { + if (comp == null || !mContext.bindServiceAsUser(intent, mConnection, 0, + android.os.Process.myUserHandle())) { Log.e(TAG, "Could not bind to Bluetooth Pan Service with " + intent); return false; } diff --git a/core/java/android/bluetooth/BluetoothPbap.java b/core/java/android/bluetooth/BluetoothPbap.java index 7f45652..dc01fc7 100644 --- a/core/java/android/bluetooth/BluetoothPbap.java +++ b/core/java/android/bluetooth/BluetoothPbap.java @@ -22,7 +22,6 @@ import android.content.Intent; import android.content.ServiceConnection; import android.os.RemoteException; import android.os.IBinder; -import android.os.ServiceManager; import android.util.Log; /** @@ -161,7 +160,8 @@ public class BluetoothPbap { Intent intent = new Intent(IBluetoothPbap.class.getName()); ComponentName comp = intent.resolveSystemService(mContext.getPackageManager(), 0); intent.setComponent(comp); - if (comp == null || !mContext.bindService(intent, mConnection, 0)) { + if (comp == null || !mContext.bindServiceAsUser(intent, mConnection, 0, + android.os.Process.myUserHandle())) { Log.e(TAG, "Could not bind to Bluetooth Pbap Service with " + intent); return false; } diff --git a/core/java/android/bluetooth/BluetoothServerSocket.java b/core/java/android/bluetooth/BluetoothServerSocket.java index 96be8a2..bc56e55 100644 --- a/core/java/android/bluetooth/BluetoothServerSocket.java +++ b/core/java/android/bluetooth/BluetoothServerSocket.java @@ -17,7 +17,6 @@ package android.bluetooth; import android.os.Handler; -import android.os.Message; import android.os.ParcelUuid; import java.io.Closeable; diff --git a/core/java/android/bluetooth/BluetoothSocket.java b/core/java/android/bluetooth/BluetoothSocket.java index 1e75fc2..f532f7c 100644 --- a/core/java/android/bluetooth/BluetoothSocket.java +++ b/core/java/android/bluetooth/BluetoothSocket.java @@ -16,21 +16,16 @@ package android.bluetooth; -import android.os.IBinder; import android.os.ParcelUuid; import android.os.ParcelFileDescriptor; import android.os.RemoteException; -import android.os.ServiceManager; import android.util.Log; import java.io.Closeable; import java.io.FileDescriptor; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.List; import java.util.Locale; import java.util.UUID; import android.net.LocalSocket; @@ -462,8 +457,10 @@ public final class BluetoothSocket implements Closeable { mSocket.close(); mSocket = null; } - if(mPfd != null) - mPfd.detachFd(); + if (mPfd != null) { + mPfd.close(); + mPfd = null; + } } } } diff --git a/core/java/android/bluetooth/BluetoothTetheringDataTracker.java b/core/java/android/bluetooth/BluetoothTetheringDataTracker.java index a9b7176..6dd551e 100644 --- a/core/java/android/bluetooth/BluetoothTetheringDataTracker.java +++ b/core/java/android/bluetooth/BluetoothTetheringDataTracker.java @@ -17,9 +17,6 @@ package android.bluetooth; import android.net.BaseNetworkStateTracker; -import android.os.IBinder; -import android.os.ServiceManager; -import android.os.INetworkManagementService; import android.content.Context; import android.net.ConnectivityManager; import android.net.DhcpResults; @@ -35,11 +32,6 @@ import android.os.Message; import android.os.Messenger; import android.text.TextUtils; import android.util.Log; -import java.net.InterfaceAddress; -import android.net.LinkAddress; -import android.net.RouteInfo; -import java.net.Inet4Address; -import android.os.SystemProperties; import com.android.internal.util.AsyncChannel; @@ -143,11 +135,6 @@ public class BluetoothTetheringDataTracker extends BaseNetworkStateTracker { } @Override - public void captivePortalCheckComplete() { - // not implemented - } - - @Override public void captivePortalCheckCompleted(boolean isCaptivePortal) { // not implemented } diff --git a/core/java/android/content/AsyncTaskLoader.java b/core/java/android/content/AsyncTaskLoader.java index eb7426e..7241e0d 100644 --- a/core/java/android/content/AsyncTaskLoader.java +++ b/core/java/android/content/AsyncTaskLoader.java @@ -20,7 +20,7 @@ import android.os.AsyncTask; import android.os.Handler; import android.os.OperationCanceledException; import android.os.SystemClock; -import android.util.Slog; +import android.util.Log; import android.util.TimeUtils; import java.io.FileDescriptor; @@ -64,10 +64,10 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { /* Runs on a worker thread */ @Override protected D doInBackground(Void... params) { - if (DEBUG) Slog.v(TAG, this + " >>> doInBackground"); + if (DEBUG) Log.v(TAG, this + " >>> doInBackground"); try { D data = AsyncTaskLoader.this.onLoadInBackground(); - if (DEBUG) Slog.v(TAG, this + " <<< doInBackground"); + if (DEBUG) Log.v(TAG, this + " <<< doInBackground"); return data; } catch (OperationCanceledException ex) { if (!isCancelled()) { @@ -79,7 +79,7 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { // So we treat this case as an unhandled exception. throw ex; } - if (DEBUG) Slog.v(TAG, this + " <<< doInBackground (was canceled)", ex); + if (DEBUG) Log.v(TAG, this + " <<< doInBackground (was canceled)", ex); return null; } } @@ -87,7 +87,7 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { /* Runs on the UI thread */ @Override protected void onPostExecute(D data) { - if (DEBUG) Slog.v(TAG, this + " onPostExecute"); + if (DEBUG) Log.v(TAG, this + " onPostExecute"); try { AsyncTaskLoader.this.dispatchOnLoadComplete(this, data); } finally { @@ -98,7 +98,7 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { /* Runs on the UI thread */ @Override protected void onCancelled(D data) { - if (DEBUG) Slog.v(TAG, this + " onCancelled"); + if (DEBUG) Log.v(TAG, this + " onCancelled"); try { AsyncTaskLoader.this.dispatchOnCancelled(this, data); } finally { @@ -162,18 +162,18 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { super.onForceLoad(); cancelLoad(); mTask = new LoadTask(); - if (DEBUG) Slog.v(TAG, "Preparing load: mTask=" + mTask); + if (DEBUG) Log.v(TAG, "Preparing load: mTask=" + mTask); executePendingTask(); } @Override protected boolean onCancelLoad() { - if (DEBUG) Slog.v(TAG, "onCancelLoad: mTask=" + mTask); + if (DEBUG) Log.v(TAG, "onCancelLoad: mTask=" + mTask); if (mTask != null) { if (mCancellingTask != null) { // There was a pending task already waiting for a previous // one being canceled; just drop it. - if (DEBUG) Slog.v(TAG, + if (DEBUG) Log.v(TAG, "cancelLoad: still waiting for cancelled task; dropping next"); if (mTask.waiting) { mTask.waiting = false; @@ -184,14 +184,14 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { } else if (mTask.waiting) { // There is a task, but it is waiting for the time it should // execute. We can just toss it. - if (DEBUG) Slog.v(TAG, "cancelLoad: task is waiting, dropping it"); + if (DEBUG) Log.v(TAG, "cancelLoad: task is waiting, dropping it"); mTask.waiting = false; mHandler.removeCallbacks(mTask); mTask = null; return false; } else { boolean cancelled = mTask.cancel(false); - if (DEBUG) Slog.v(TAG, "cancelLoad: cancelled=" + cancelled); + if (DEBUG) Log.v(TAG, "cancelLoad: cancelled=" + cancelled); if (cancelled) { mCancellingTask = mTask; cancelLoadInBackground(); @@ -223,7 +223,7 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { long now = SystemClock.uptimeMillis(); if (now < (mLastLoadCompleteTime+mUpdateThrottle)) { // Not yet time to do another load. - if (DEBUG) Slog.v(TAG, "Waiting until " + if (DEBUG) Log.v(TAG, "Waiting until " + (mLastLoadCompleteTime+mUpdateThrottle) + " to execute: " + mTask); mTask.waiting = true; @@ -231,7 +231,7 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { return; } } - if (DEBUG) Slog.v(TAG, "Executing: " + mTask); + if (DEBUG) Log.v(TAG, "Executing: " + mTask); mTask.executeOnExecutor(mExecutor, (Void[]) null); } } @@ -239,11 +239,11 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { void dispatchOnCancelled(LoadTask task, D data) { onCanceled(data); if (mCancellingTask == task) { - if (DEBUG) Slog.v(TAG, "Cancelled task is now canceled!"); + if (DEBUG) Log.v(TAG, "Cancelled task is now canceled!"); rollbackContentChanged(); mLastLoadCompleteTime = SystemClock.uptimeMillis(); mCancellingTask = null; - if (DEBUG) Slog.v(TAG, "Delivering cancellation"); + if (DEBUG) Log.v(TAG, "Delivering cancellation"); deliverCancellation(); executePendingTask(); } @@ -251,7 +251,7 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { void dispatchOnLoadComplete(LoadTask task, D data) { if (mTask != task) { - if (DEBUG) Slog.v(TAG, "Load complete of old task, trying to cancel"); + if (DEBUG) Log.v(TAG, "Load complete of old task, trying to cancel"); dispatchOnCancelled(task, data); } else { if (isAbandoned()) { @@ -261,7 +261,7 @@ public abstract class AsyncTaskLoader<D> extends Loader<D> { commitContentChanged(); mLastLoadCompleteTime = SystemClock.uptimeMillis(); mTask = null; - if (DEBUG) Slog.v(TAG, "Delivering result"); + if (DEBUG) Log.v(TAG, "Delivering result"); deliverResult(data); } } diff --git a/core/java/android/content/ClipboardManager.java b/core/java/android/content/ClipboardManager.java index 73e6fd0..5653cad 100644 --- a/core/java/android/content/ClipboardManager.java +++ b/core/java/android/content/ClipboardManager.java @@ -22,8 +22,6 @@ import android.os.RemoteException; import android.os.Handler; import android.os.IBinder; import android.os.ServiceManager; -import android.os.StrictMode; -import android.util.Log; import java.util.ArrayList; diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java index 2bf4d7d..f3c803d 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -141,7 +141,7 @@ public abstract class ContentResolver { public static final String SYNC_EXTRAS_PRIORITY = "sync_priority"; /** {@hide} Flag to allow sync to occur on metered network. */ - public static final String SYNC_EXTRAS_DISALLOW_METERED = "disallow_metered"; + public static final String SYNC_EXTRAS_DISALLOW_METERED = "allow_metered"; /** * Set by the SyncManager to request that the SyncAdapter initialize itself for @@ -192,6 +192,14 @@ public abstract class ContentResolver { */ public static final String CURSOR_DIR_BASE_TYPE = "vnd.android.cursor.dir"; + /** + * This is the Android platform's generic MIME type to match any MIME + * type of the form "{@link #CURSOR_ITEM_BASE_TYPE}/{@code SUB_TYPE}". + * {@code SUB_TYPE} is the sub-type of the application-dependent + * content, e.g., "audio", "video", "playlist". + */ + public static final String ANY_CURSOR_ITEM_TYPE = "vnd.android.cursor.item/*"; + /** @hide */ public static final int SYNC_ERROR_SYNC_ALREADY_IN_PROGRESS = 1; /** @hide */ @@ -368,9 +376,7 @@ public abstract class ContentResolver { } /** - * <p> * Query the given URI, returning a {@link Cursor} over the result set. - * </p> * <p> * For best performance, the caller should follow these guidelines: * <ul> @@ -405,9 +411,8 @@ public abstract class ContentResolver { } /** - * <p> - * Query the given URI, returning a {@link Cursor} over the result set. - * </p> + * Query the given URI, returning a {@link Cursor} over the result set + * with optional support for cancellation. * <p> * For best performance, the caller should follow these guidelines: * <ul> @@ -1751,7 +1756,7 @@ public abstract class ContentResolver { new SyncRequest.Builder() .setSyncAdapter(account, authority) .setExtras(extras) - .syncOnce() + .syncOnce() // Immediate sync. .build(); requestSync(request); } @@ -1759,9 +1764,6 @@ public abstract class ContentResolver { /** * Register a sync with the SyncManager. These requests are built using the * {@link SyncRequest.Builder}. - * - * @param request The immutable SyncRequest object containing the sync parameters. Use - * {@link SyncRequest.Builder} to construct these. */ public static void requestSync(SyncRequest request) { try { @@ -1829,8 +1831,21 @@ public abstract class ContentResolver { */ public static void cancelSync(Account account, String authority) { try { - getContentService().cancelSync(account, authority); + getContentService().cancelSync(account, authority, null); + } catch (RemoteException e) { + } + } + + /** + * Cancel any active or pending syncs that are running on this service. + * + * @param cname the service for which to cancel all active/pending operations. + */ + public static void cancelSync(ComponentName cname) { + try { + getContentService().cancelSync(null, null, cname); } catch (RemoteException e) { + } } @@ -1897,12 +1912,13 @@ public abstract class ContentResolver { * {@link #SYNC_EXTRAS_INITIALIZE}, {@link #SYNC_EXTRAS_FORCE}, * {@link #SYNC_EXTRAS_EXPEDITED}, {@link #SYNC_EXTRAS_MANUAL} set to true. * If any are supplied then an {@link IllegalArgumentException} will be thrown. - * <p>As of API level 19 this function introduces a default flexibility of ~4% (up to a maximum - * of one hour in the day) into the requested period. Use - * {@link SyncRequest.Builder#syncPeriodic(long, long)} to set this flexibility manually. * * <p>This method requires the caller to hold the permission * {@link android.Manifest.permission#WRITE_SYNC_SETTINGS}. + * <p>The bundle for a periodic sync can be queried by applications with the correct + * permissions using + * {@link ContentResolver#getPeriodicSyncs(Account account, String provider)}, so no + * sensitive data should be transferred here. * * @param account the account to specify in the sync * @param authority the provider to specify in the sync request @@ -1932,6 +1948,26 @@ public abstract class ContentResolver { } /** + * {@hide} + * Helper function to throw an <code>IllegalArgumentException</code> if any illegal + * extras were set for a periodic sync. + * + * @param extras bundle to validate. + */ + public static boolean invalidPeriodicExtras(Bundle extras) { + if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false) + || extras.getBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, false) + || extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false) + || extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, false) + || extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false) + || extras.getBoolean(ContentResolver.SYNC_EXTRAS_FORCE, false) + || extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false)) { + return true; + } + return false; + } + + /** * Remove a periodic sync. Has no affect if account, authority and extras don't match * an existing periodic sync. * <p>This method requires the caller to hold the permission @@ -1951,6 +1987,31 @@ public abstract class ContentResolver { } /** + * Remove the specified sync. This will cancel any pending or active syncs. If the request is + * for a periodic sync, this call will remove any future occurrences. + * <p>If a periodic sync is specified, the caller must hold the permission + * {@link android.Manifest.permission#WRITE_SYNC_SETTINGS}. If this SyncRequest targets a + * SyncService adapter,the calling application must be signed with the same certificate as the + * adapter. + *</p>It is possible to cancel a sync using a SyncRequest object that is not the same object + * with which you requested the sync. Do so by building a SyncRequest with the same + * service/adapter, frequency, <b>and</b> extras bundle. + * + * @param request SyncRequest object containing information about sync to cancel. + */ + public static void cancelSync(SyncRequest request) { + if (request == null) { + throw new IllegalArgumentException("request cannot be null"); + } + try { + getContentService().cancelRequest(request); + } catch (RemoteException e) { + // exception ignored; if this is thrown then it means the runtime is in the midst of + // being restarted + } + } + + /** * Get the list of information about the periodic syncs for the given account and authority. * <p>This method requires the caller to hold the permission * {@link android.Manifest.permission#READ_SYNC_SETTINGS}. @@ -1961,7 +2022,23 @@ public abstract class ContentResolver { */ public static List<PeriodicSync> getPeriodicSyncs(Account account, String authority) { try { - return getContentService().getPeriodicSyncs(account, authority); + return getContentService().getPeriodicSyncs(account, authority, null); + } catch (RemoteException e) { + throw new RuntimeException("the ContentService should always be reachable", e); + } + } + + /** + * Return periodic syncs associated with the provided component. + * <p>The calling application must be signed with the same certificate as the target component, + * otherwise this call will fail. + */ + public static List<PeriodicSync> getPeriodicSyncs(ComponentName cname) { + if (cname == null) { + throw new IllegalArgumentException("Component must not be null"); + } + try { + return getContentService().getPeriodicSyncs(null, null, cname); } catch (RemoteException e) { throw new RuntimeException("the ContentService should always be reachable", e); } @@ -1997,6 +2074,38 @@ public abstract class ContentResolver { } /** + * Set whether the provided {@link SyncService} is available to process work. + * <p>This method requires the caller to hold the permission + * {@link android.Manifest.permission#WRITE_SYNC_SETTINGS}. + * <p>The calling application must be signed with the same certificate as the target component, + * otherwise this call will fail. + */ + public static void setServiceActive(ComponentName cname, boolean active) { + try { + getContentService().setServiceActive(cname, active); + } catch (RemoteException e) { + // exception ignored; if this is thrown then it means the runtime is in the midst of + // being restarted + } + } + + /** + * Query the state of this sync service. + * <p>Set with {@link #setServiceActive(ComponentName cname, boolean active)}. + * <p>The calling application must be signed with the same certificate as the target component, + * otherwise this call will fail. + * @param cname ComponentName referring to a {@link SyncService} + * @return true if jobs will be run on this service, false otherwise. + */ + public static boolean isServiceActive(ComponentName cname) { + try { + return getContentService().isServiceActive(cname); + } catch (RemoteException e) { + throw new RuntimeException("the ContentService should always be reachable", e); + } + } + + /** * Gets the master auto-sync setting that applies to all the providers and accounts. * If this is false then the per-provider auto-sync setting is ignored. * <p>This method requires the caller to hold the permission @@ -2030,8 +2139,8 @@ public abstract class ContentResolver { } /** - * Returns true if there is currently a sync operation for the given - * account or authority in the pending list, or actively being processed. + * Returns true if there is currently a sync operation for the given account or authority + * actively being processed. * <p>This method requires the caller to hold the permission * {@link android.Manifest.permission#READ_SYNC_STATS}. * @param account the account whose setting we are querying @@ -2039,8 +2148,26 @@ public abstract class ContentResolver { * @return true if a sync is active for the given account or authority. */ public static boolean isSyncActive(Account account, String authority) { + if (account == null) { + throw new IllegalArgumentException("account must not be null"); + } + if (authority == null) { + throw new IllegalArgumentException("authority must not be null"); + } + try { - return getContentService().isSyncActive(account, authority); + return getContentService().isSyncActive(account, authority, null); + } catch (RemoteException e) { + throw new RuntimeException("the ContentService should always be reachable", e); + } + } + + public static boolean isSyncActive(ComponentName cname) { + if (cname == null) { + throw new IllegalArgumentException("component name must not be null"); + } + try { + return getContentService().isSyncActive(null, null, cname); } catch (RemoteException e) { throw new RuntimeException("the ContentService should always be reachable", e); } @@ -2098,7 +2225,7 @@ public abstract class ContentResolver { */ public static SyncStatusInfo getSyncStatus(Account account, String authority) { try { - return getContentService().getSyncStatus(account, authority); + return getContentService().getSyncStatus(account, authority, null); } catch (RemoteException e) { throw new RuntimeException("the ContentService should always be reachable", e); } @@ -2114,7 +2241,15 @@ public abstract class ContentResolver { */ public static boolean isSyncPending(Account account, String authority) { try { - return getContentService().isSyncPending(account, authority); + return getContentService().isSyncPending(account, authority, null); + } catch (RemoteException e) { + throw new RuntimeException("the ContentService should always be reachable", e); + } + } + + public static boolean isSyncPending(ComponentName cname) { + try { + return getContentService().isSyncPending(null, null, cname); } catch (RemoteException e) { throw new RuntimeException("the ContentService should always be reachable", e); } diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 5057cf4..ff92d82 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -16,6 +16,10 @@ package android.content; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.StringDef; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.AssetManager; @@ -47,6 +51,8 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * Interface to global information about an application environment. This is @@ -132,6 +138,20 @@ public abstract class Context { */ public static final int MODE_ENABLE_WRITE_AHEAD_LOGGING = 0x0008; + /** @hide */ + @IntDef(flag = true, + value = { + BIND_AUTO_CREATE, + BIND_AUTO_CREATE, + BIND_DEBUG_UNBIND, + BIND_NOT_FOREGROUND, + BIND_ABOVE_CLIENT, + BIND_ALLOW_OOM_MANAGEMENT, + BIND_WAIVE_PRIORITY + }) + @Retention(RetentionPolicy.SOURCE) + public @interface BindServiceFlags {} + /** * Flag for {@link #bindService}: automatically create the service as long * as the binding exists. Note that while this will create the service, @@ -222,6 +242,16 @@ public abstract class Context { public static final int BIND_ADJUST_WITH_ACTIVITY = 0x0080; /** + * @hide Flag for {@link #bindService}: Treat the binding as hosting + * an activity, an unbinding as the activity going in the background. + * That is, when unbinding, the process when empty will go on the activity + * LRU list instead of the regular one, keeping it around more aggressively + * than it otherwise would be. This is intended for use with IMEs to try + * to keep IME processes around for faster keyboard switching. + */ + public static final int BIND_TREAT_LIKE_ACTIVITY = 0x08000000; + + /** * @hide An idea that is not yet implemented. * Flag for {@link #bindService}: If binding from an activity, consider * this service to be visible like the binding activity is. That is, @@ -356,6 +386,19 @@ public abstract class Context { return getResources().getString(resId, formatArgs); } + /** + * Return a drawable object associated with a particular resource ID and + * styled for the current theme. + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * @return Drawable An object that can be used to draw this resource. + */ + public final Drawable getDrawable(int id) { + return getResources().getDrawable(id, getTheme()); + } + /** * Set the base theme for this context. Note that this should be called * before any views are instantiated in the Context (for example before @@ -495,7 +538,7 @@ public abstract class Context { * and {@link #MODE_WORLD_WRITEABLE} to control permissions. The bit * {@link #MODE_MULTI_PROCESS} can also be used if multiple processes * are mutating the same SharedPreferences file. {@link #MODE_MULTI_PROCESS} - * is always on in apps targetting Gingerbread (Android 2.3) and below, and + * is always on in apps targeting Gingerbread (Android 2.3) and below, and * off by default in later versions. * * @return The single {@link SharedPreferences} instance that can be used @@ -680,7 +723,8 @@ public abstract class Context { * @see #getFilesDir * @see android.os.Environment#getExternalStoragePublicDirectory */ - public abstract File getExternalFilesDir(String type); + @Nullable + public abstract File getExternalFilesDir(@Nullable String type); /** * Returns absolute paths to application-specific directories on all @@ -713,7 +757,7 @@ public abstract class Context { * Returned paths may be {@code null} if a storage device is unavailable. * * @see #getExternalFilesDir(String) - * @see Environment#getStorageState(File) + * @see Environment#getExternalStorageState(File) */ public abstract File[] getExternalFilesDirs(String type); @@ -777,7 +821,7 @@ public abstract class Context { * Returned paths may be {@code null} if a storage device is unavailable. * * @see #getObbDir() - * @see Environment#getStorageState(File) + * @see Environment#getExternalStorageState(File) */ public abstract File[] getObbDirs(); @@ -846,6 +890,7 @@ public abstract class Context { * * @see #getCacheDir */ + @Nullable public abstract File getExternalCacheDir(); /** @@ -879,7 +924,7 @@ public abstract class Context { * Returned paths may be {@code null} if a storage device is unavailable. * * @see #getExternalCacheDir() - * @see Environment#getStorageState(File) + * @see Environment#getExternalStorageState(File) */ public abstract File[] getExternalCacheDirs(); @@ -966,7 +1011,8 @@ public abstract class Context { * @see #deleteDatabase */ public abstract SQLiteDatabase openOrCreateDatabase(String name, - int mode, CursorFactory factory, DatabaseErrorHandler errorHandler); + int mode, CursorFactory factory, + @Nullable DatabaseErrorHandler errorHandler); /** * Delete an existing private SQLiteDatabase associated with this Context's @@ -1112,7 +1158,7 @@ public abstract class Context { * @see #startActivity(Intent) * @see PackageManager#resolveActivity */ - public abstract void startActivity(Intent intent, Bundle options); + public abstract void startActivity(Intent intent, @Nullable Bundle options); /** * Version of {@link #startActivity(Intent, Bundle)} that allows you to specify the @@ -1128,7 +1174,7 @@ public abstract class Context { * @throws ActivityNotFoundException * @hide */ - public void startActivityAsUser(Intent intent, Bundle options, UserHandle userId) { + public void startActivityAsUser(Intent intent, @Nullable Bundle options, UserHandle userId) { throw new RuntimeException("Not implemented. Must override in a subclass."); } @@ -1247,7 +1293,7 @@ public abstract class Context { * @see #startIntentSender(IntentSender, Intent, int, int, int) */ public abstract void startIntentSender(IntentSender intent, - Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags, + @Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags, Bundle options) throws IntentSender.SendIntentException; /** @@ -1297,11 +1343,11 @@ public abstract class Context { * @see #sendOrderedBroadcast(Intent, String, BroadcastReceiver, Handler, int, String, Bundle) */ public abstract void sendBroadcast(Intent intent, - String receiverPermission); + @Nullable String receiverPermission); /** * Like {@link #sendBroadcast(Intent, String)}, but also allows specification - * of an assocated app op as per {@link android.app.AppOpsManager}. + * of an associated app op as per {@link android.app.AppOpsManager}. * @hide */ public abstract void sendBroadcast(Intent intent, @@ -1328,7 +1374,7 @@ public abstract class Context { * @see #sendOrderedBroadcast(Intent, String, BroadcastReceiver, Handler, int, String, Bundle) */ public abstract void sendOrderedBroadcast(Intent intent, - String receiverPermission); + @Nullable String receiverPermission); /** * Version of {@link #sendBroadcast(Intent)} that allows you to @@ -1372,15 +1418,15 @@ public abstract class Context { * @see #registerReceiver * @see android.app.Activity#RESULT_OK */ - public abstract void sendOrderedBroadcast(Intent intent, - String receiverPermission, BroadcastReceiver resultReceiver, - Handler scheduler, int initialCode, String initialData, - Bundle initialExtras); + public abstract void sendOrderedBroadcast(@NonNull Intent intent, + @Nullable String receiverPermission, BroadcastReceiver resultReceiver, + @Nullable Handler scheduler, int initialCode, @Nullable String initialData, + @Nullable Bundle initialExtras); /** * Like {@link #sendOrderedBroadcast(Intent, String, BroadcastReceiver, android.os.Handler, * int, String, android.os.Bundle)}, but also allows specification - * of an assocated app op as per {@link android.app.AppOpsManager}. + * of an associated app op as per {@link android.app.AppOpsManager}. * @hide */ public abstract void sendOrderedBroadcast(Intent intent, @@ -1415,7 +1461,7 @@ public abstract class Context { * @see #sendBroadcast(Intent, String) */ public abstract void sendBroadcastAsUser(Intent intent, UserHandle user, - String receiverPermission); + @Nullable String receiverPermission); /** * Version of @@ -1448,8 +1494,9 @@ public abstract class Context { * @see #sendOrderedBroadcast(Intent, String, BroadcastReceiver, Handler, int, String, Bundle) */ public abstract void sendOrderedBroadcastAsUser(Intent intent, UserHandle user, - String receiverPermission, BroadcastReceiver resultReceiver, Handler scheduler, - int initialCode, String initialData, Bundle initialExtras); + @Nullable String receiverPermission, BroadcastReceiver resultReceiver, + @Nullable Handler scheduler, int initialCode, @Nullable String initialData, + @Nullable Bundle initialExtras); /** * Perform a {@link #sendBroadcast(Intent)} that is "sticky," meaning the @@ -1514,8 +1561,8 @@ public abstract class Context { */ public abstract void sendStickyOrderedBroadcast(Intent intent, BroadcastReceiver resultReceiver, - Handler scheduler, int initialCode, String initialData, - Bundle initialExtras); + @Nullable Handler scheduler, int initialCode, @Nullable String initialData, + @Nullable Bundle initialExtras); /** * Remove the data previously sent with {@link #sendStickyBroadcast}, @@ -1575,8 +1622,8 @@ public abstract class Context { */ public abstract void sendStickyOrderedBroadcastAsUser(Intent intent, UserHandle user, BroadcastReceiver resultReceiver, - Handler scheduler, int initialCode, String initialData, - Bundle initialExtras); + @Nullable Handler scheduler, int initialCode, @Nullable String initialData, + @Nullable Bundle initialExtras); /** * Version of {@link #removeStickyBroadcast(Intent)} that allows you to specify the @@ -1643,7 +1690,8 @@ public abstract class Context { * @see #sendBroadcast * @see #unregisterReceiver */ - public abstract Intent registerReceiver(BroadcastReceiver receiver, + @Nullable + public abstract Intent registerReceiver(@Nullable BroadcastReceiver receiver, IntentFilter filter); /** @@ -1677,8 +1725,10 @@ public abstract class Context { * @see #sendBroadcast * @see #unregisterReceiver */ + @Nullable public abstract Intent registerReceiver(BroadcastReceiver receiver, - IntentFilter filter, String broadcastPermission, Handler scheduler); + IntentFilter filter, @Nullable String broadcastPermission, + @Nullable Handler scheduler); /** * @hide @@ -1704,9 +1754,10 @@ public abstract class Context { * @see #sendBroadcast * @see #unregisterReceiver */ + @Nullable public abstract Intent registerReceiverAsUser(BroadcastReceiver receiver, - UserHandle user, IntentFilter filter, String broadcastPermission, - Handler scheduler); + UserHandle user, IntentFilter filter, @Nullable String broadcastPermission, + @Nullable Handler scheduler); /** * Unregister a previously registered BroadcastReceiver. <em>All</em> @@ -1765,6 +1816,7 @@ public abstract class Context { * @see #stopService * @see #bindService */ + @Nullable public abstract ComponentName startService(Intent service); /** @@ -1852,8 +1904,8 @@ public abstract class Context { * @see #BIND_DEBUG_UNBIND * @see #BIND_NOT_FOREGROUND */ - public abstract boolean bindService(Intent service, ServiceConnection conn, - int flags); + public abstract boolean bindService(Intent service, @NonNull ServiceConnection conn, + @BindServiceFlags int flags); /** * Same as {@link #bindService(Intent, ServiceConnection, int)}, but with an explicit userHandle @@ -1874,7 +1926,7 @@ public abstract class Context { * * @see #bindService */ - public abstract void unbindService(ServiceConnection conn); + public abstract void unbindService(@NonNull ServiceConnection conn); /** * Start executing an {@link android.app.Instrumentation} class. The given @@ -1899,8 +1951,68 @@ public abstract class Context { * @return {@code true} if the instrumentation was successfully started, * else {@code false} if it could not be found. */ - public abstract boolean startInstrumentation(ComponentName className, - String profileFile, Bundle arguments); + public abstract boolean startInstrumentation(@NonNull ComponentName className, + @Nullable String profileFile, @Nullable Bundle arguments); + + /** @hide */ + @StringDef({ + POWER_SERVICE, + WINDOW_SERVICE, + LAYOUT_INFLATER_SERVICE, + ACCOUNT_SERVICE, + ACTIVITY_SERVICE, + ALARM_SERVICE, + NOTIFICATION_SERVICE, + ACCESSIBILITY_SERVICE, + CAPTIONING_SERVICE, + KEYGUARD_SERVICE, + LOCATION_SERVICE, + //@hide: COUNTRY_DETECTOR, + SEARCH_SERVICE, + SENSOR_SERVICE, + STORAGE_SERVICE, + WALLPAPER_SERVICE, + VIBRATOR_SERVICE, + //@hide: STATUS_BAR_SERVICE, + CONNECTIVITY_SERVICE, + //@hide: UPDATE_LOCK_SERVICE, + //@hide: NETWORKMANAGEMENT_SERVICE, + //@hide: NETWORK_STATS_SERVICE, + //@hide: NETWORK_POLICY_SERVICE, + WIFI_SERVICE, + WIFI_HOTSPOT_SERVICE, + WIFI_P2P_SERVICE, + NSD_SERVICE, + AUDIO_SERVICE, + MEDIA_ROUTER_SERVICE, + TELEPHONY_SERVICE, + CLIPBOARD_SERVICE, + INPUT_METHOD_SERVICE, + TEXT_SERVICES_MANAGER_SERVICE, + //@hide: APPWIDGET_SERVICE, + //@hide: BACKUP_SERVICE, + DROPBOX_SERVICE, + DEVICE_POLICY_SERVICE, + UI_MODE_SERVICE, + DOWNLOAD_SERVICE, + NFC_SERVICE, + BLUETOOTH_SERVICE, + //@hide: SIP_SERVICE, + USB_SERVICE, + LAUNCHER_APPS_SERVICE, + //@hide: SERIAL_SERVICE, + INPUT_SERVICE, + DISPLAY_SERVICE, + //@hide: SCHEDULING_POLICY_SERVICE, + USER_SERVICE, + //@hide: APP_OPS_SERVICE + CAMERA_SERVICE, + PRINT_SERVICE, + MEDIA_SESSION_SERVICE, + BATTERY_SERVICE, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ServiceName {} /** * Return the handle to a system-level service by name. The class of the @@ -1949,6 +2061,8 @@ public abstract class Context { * <dd> An {@link android.app.UiModeManager} for controlling UI modes. * <dt> {@link #DOWNLOAD_SERVICE} ("download") * <dd> A {@link android.app.DownloadManager} for requesting HTTP downloads + * <dt> {@link #BATTERY_SERVICE} ("batterymanager") + * <dd> A {@link android.os.BatteryManager} for managing battery state * </dl> * * <p>Note: System services obtained via this API may be closely associated with @@ -2002,8 +2116,10 @@ public abstract class Context { * @see android.app.UiModeManager * @see #DOWNLOAD_SERVICE * @see android.app.DownloadManager + * @see #BATTERY_SERVICE + * @see android.os.BatteryManager */ - public abstract Object getSystemService(String name); + public abstract Object getSystemService(@ServiceName @NonNull String name); /** * Use with {@link #getSystemService} to retrieve a @@ -2221,6 +2337,16 @@ public abstract class Context { /** * Use with {@link #getSystemService} to retrieve a {@link + * android.net.wifi.hotspot.WifiHotspotManager} for handling management of + * Wi-Fi hotspot access. + * + * @see #getSystemService + * @see android.net.wifi.hotspot.WifiHotspotManager + */ + public static final String WIFI_HOTSPOT_SERVICE = "wifihotspot"; + + /** + * Use with {@link #getSystemService} to retrieve a {@link * android.net.wifi.p2p.WifiP2pManager} for handling management of * Wi-Fi peer-to-peer connections. * @@ -2261,6 +2387,15 @@ public abstract class Context { /** * Use with {@link #getSystemService} to retrieve a + * {@link android.media.session.MediaSessionManager} for managing media Sessions. + * + * @see #getSystemService + * @see android.media.session.MediaSessionManager + */ + public static final String MEDIA_SESSION_SERVICE = "media_session"; + + /** + * Use with {@link #getSystemService} to retrieve a * {@link android.telephony.TelephonyManager} for handling management the * telephony features of the device. * @@ -2351,6 +2486,14 @@ public abstract class Context { /** * Use with {@link #getSystemService} to retrieve a + * {@link android.os.BatteryManager} for managing battery state. + * + * @see #getSystemService + */ + public static final String BATTERY_SERVICE = "batterymanager"; + + /** + * Use with {@link #getSystemService} to retrieve a * {@link android.nfc.NfcManager} for using NFC. * * @see #getSystemService @@ -2434,6 +2577,16 @@ public abstract class Context { /** * Use with {@link #getSystemService} to retrieve a + * {@link android.content.pm.LauncherApps} for querying and monitoring launchable apps across + * profiles of a user. + * + * @see #getSystemService + * @see android.content.pm.LauncherApps + */ + public static final String LAUNCHER_APPS_SERVICE = "launcherapps"; + + /** + * Use with {@link #getSystemService} to retrieve a * {@link android.app.AppOpsManager} for tracking application operations * on the device. * @@ -2449,7 +2602,6 @@ public abstract class Context { * * @see #getSystemService * @see android.hardware.camera2.CameraManager - * @hide */ public static final String CAMERA_SERVICE = "camera"; @@ -2473,6 +2625,32 @@ public abstract class Context { public static final String CONSUMER_IR_SERVICE = "consumer_ir"; /** + * {@link android.app.trust.TrustManager} for managing trust agents. + * @see #getSystemService + * @see android.app.trust.TrustManager + * @hide + */ + public static final String TRUST_SERVICE = "trust"; + + /** + * Use with {@link #getSystemService} to retrieve a + * {@link android.tv.TvInputManager} for interacting with TV inputs on the + * device. + * + * @see #getSystemService + * @see android.tv.TvInputManager + */ + public static final String TV_INPUT_SERVICE = "tv_input"; + + /** + * {@link android.net.NetworkScoreManager} for managing network scoring. + * @see #getSystemService + * @see android.net.NetworkScoreManager + * @hide + */ + public static final String NETWORK_SCORE_SERVICE = "network_score"; + + /** * Determine whether the given permission is allowed for a particular * process and user ID running in the system. * @@ -2488,7 +2666,8 @@ public abstract class Context { * @see PackageManager#checkPermission(String, String) * @see #checkCallingPermission */ - public abstract int checkPermission(String permission, int pid, int uid); + @PackageManager.PermissionResult + public abstract int checkPermission(@NonNull String permission, int pid, int uid); /** * Determine whether the calling process of an IPC you are handling has been @@ -2511,7 +2690,8 @@ public abstract class Context { * @see #checkPermission * @see #checkCallingOrSelfPermission */ - public abstract int checkCallingPermission(String permission); + @PackageManager.PermissionResult + public abstract int checkCallingPermission(@NonNull String permission); /** * Determine whether the calling process of an IPC <em>or you</em> have been @@ -2529,7 +2709,8 @@ public abstract class Context { * @see #checkPermission * @see #checkCallingPermission */ - public abstract int checkCallingOrSelfPermission(String permission); + @PackageManager.PermissionResult + public abstract int checkCallingOrSelfPermission(@NonNull String permission); /** * If the given permission is not allowed for a particular process @@ -2544,7 +2725,7 @@ public abstract class Context { * @see #checkPermission(String, int, int) */ public abstract void enforcePermission( - String permission, int pid, int uid, String message); + @NonNull String permission, int pid, int uid, @Nullable String message); /** * If the calling process of an IPC you are handling has not been @@ -2565,7 +2746,7 @@ public abstract class Context { * @see #checkCallingPermission(String) */ public abstract void enforceCallingPermission( - String permission, String message); + @NonNull String permission, @Nullable String message); /** * If neither you nor the calling process of an IPC you are @@ -2581,7 +2762,7 @@ public abstract class Context { * @see #checkCallingOrSelfPermission(String) */ public abstract void enforceCallingOrSelfPermission( - String permission, String message); + @NonNull String permission, @Nullable String message); /** * Grant permission to access a specific Uri to another package, regardless @@ -2617,7 +2798,7 @@ public abstract class Context { * @see #revokeUriPermission */ public abstract void grantUriPermission(String toPackage, Uri uri, - int modeFlags); + @Intent.GrantUriMode int modeFlags); /** * Remove all permissions to access a particular content provider Uri @@ -2636,7 +2817,7 @@ public abstract class Context { * * @see #grantUriPermission */ - public abstract void revokeUriPermission(Uri uri, int modeFlags); + public abstract void revokeUriPermission(Uri uri, @Intent.GrantUriMode int modeFlags); /** * Determine whether a particular process and user ID has been granted @@ -2659,7 +2840,8 @@ public abstract class Context { * * @see #checkCallingUriPermission */ - public abstract int checkUriPermission(Uri uri, int pid, int uid, int modeFlags); + public abstract int checkUriPermission(Uri uri, int pid, int uid, + @Intent.GrantUriMode int modeFlags); /** * Determine whether the calling process and user ID has been @@ -2682,7 +2864,7 @@ public abstract class Context { * * @see #checkUriPermission(Uri, int, int, int) */ - public abstract int checkCallingUriPermission(Uri uri, int modeFlags); + public abstract int checkCallingUriPermission(Uri uri, @Intent.GrantUriMode int modeFlags); /** * Determine whether the calling process of an IPC <em>or you</em> has been granted @@ -2701,7 +2883,8 @@ public abstract class Context { * * @see #checkCallingUriPermission */ - public abstract int checkCallingOrSelfUriPermission(Uri uri, int modeFlags); + public abstract int checkCallingOrSelfUriPermission(Uri uri, + @Intent.GrantUriMode int modeFlags); /** * Check both a Uri and normal permission. This allows you to perform @@ -2713,7 +2896,7 @@ public abstract class Context { * @param readPermission The permission that provides overall read access, * or null to not do this check. * @param writePermission The permission that provides overall write - * acess, or null to not do this check. + * access, or null to not do this check. * @param pid The process ID being checked against. Must be > 0. * @param uid The user ID being checked against. A uid of 0 is the root * user, which will pass every permission check. @@ -2725,8 +2908,9 @@ public abstract class Context { * is allowed to access that uri or holds one of the given permissions, or * {@link PackageManager#PERMISSION_DENIED} if it is not. */ - public abstract int checkUriPermission(Uri uri, String readPermission, - String writePermission, int pid, int uid, int modeFlags); + public abstract int checkUriPermission(@Nullable Uri uri, @Nullable String readPermission, + @Nullable String writePermission, int pid, int uid, + @Intent.GrantUriMode int modeFlags); /** * If a particular process and user ID has not been granted @@ -2748,7 +2932,7 @@ public abstract class Context { * @see #checkUriPermission(Uri, int, int, int) */ public abstract void enforceUriPermission( - Uri uri, int pid, int uid, int modeFlags, String message); + Uri uri, int pid, int uid, @Intent.GrantUriMode int modeFlags, String message); /** * If the calling process and user ID has not been granted @@ -2770,7 +2954,7 @@ public abstract class Context { * @see #checkCallingUriPermission(Uri, int) */ public abstract void enforceCallingUriPermission( - Uri uri, int modeFlags, String message); + Uri uri, @Intent.GrantUriMode int modeFlags, String message); /** * If the calling process of an IPC <em>or you</em> has not been @@ -2789,7 +2973,7 @@ public abstract class Context { * @see #checkCallingOrSelfUriPermission(Uri, int) */ public abstract void enforceCallingOrSelfUriPermission( - Uri uri, int modeFlags, String message); + Uri uri, @Intent.GrantUriMode int modeFlags, String message); /** * Enforce both a Uri and normal permission. This allows you to perform @@ -2801,7 +2985,7 @@ public abstract class Context { * @param readPermission The permission that provides overall read access, * or null to not do this check. * @param writePermission The permission that provides overall write - * acess, or null to not do this check. + * access, or null to not do this check. * @param pid The process ID being checked against. Must be > 0. * @param uid The user ID being checked against. A uid of 0 is the root * user, which will pass every permission check. @@ -2813,8 +2997,15 @@ public abstract class Context { * @see #checkUriPermission(Uri, String, String, int, int, int) */ public abstract void enforceUriPermission( - Uri uri, String readPermission, String writePermission, - int pid, int uid, int modeFlags, String message); + @Nullable Uri uri, @Nullable String readPermission, + @Nullable String writePermission, int pid, int uid, @Intent.GrantUriMode int modeFlags, + @Nullable String message); + + /** @hide */ + @IntDef(flag = true, + value = {CONTEXT_INCLUDE_CODE, CONTEXT_IGNORE_SECURITY, CONTEXT_RESTRICTED}) + @Retention(RetentionPolicy.SOURCE) + public @interface CreatePackageOptions {} /** * Flag for use with {@link #createPackageContext}: include the application @@ -2872,7 +3063,7 @@ public abstract class Context { * the given package name. */ public abstract Context createPackageContext(String packageName, - int flags) throws PackageManager.NameNotFoundException; + @CreatePackageOptions int flags) throws PackageManager.NameNotFoundException; /** * Similar to {@link #createPackageContext(String, int)}, but with a @@ -2908,7 +3099,8 @@ public abstract class Context { * * @return A {@link Context} with the given configuration override. */ - public abstract Context createConfigurationContext(Configuration overrideConfiguration); + public abstract Context createConfigurationContext( + @NonNull Configuration overrideConfiguration); /** * Return a new Context object for the current Context but whose resources @@ -2928,7 +3120,7 @@ public abstract class Context { * * @return A {@link Context} for the display. */ - public abstract Context createDisplayContext(Display display); + public abstract Context createDisplayContext(@NonNull Display display); /** * Gets the display adjustments holder for this context. This information diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java index a708dad..93f6cdf 100644 --- a/core/java/android/content/ContextWrapper.java +++ b/core/java/android/content/ContextWrapper.java @@ -16,9 +16,6 @@ package android.content; -import android.app.Activity; -import android.app.ActivityManagerNative; -import android.app.LoadedApk; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.AssetManager; @@ -33,7 +30,6 @@ import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import android.os.RemoteException; import android.os.UserHandle; import android.view.DisplayAdjustments; import android.view.Display; diff --git a/core/java/android/content/CursorLoader.java b/core/java/android/content/CursorLoader.java index 5d7d677..c78871c 100644 --- a/core/java/android/content/CursorLoader.java +++ b/core/java/android/content/CursorLoader.java @@ -16,7 +16,6 @@ package android.content; -import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; import android.os.CancellationSignal; diff --git a/core/java/android/content/Entity.java b/core/java/android/content/Entity.java index 7842de0..607cb3f 100644 --- a/core/java/android/content/Entity.java +++ b/core/java/android/content/Entity.java @@ -16,10 +16,7 @@ package android.content; -import android.os.Parcelable; -import android.os.Parcel; import android.net.Uri; -import android.util.Log; import java.util.ArrayList; diff --git a/core/java/android/content/IContentService.aidl b/core/java/android/content/IContentService.aidl index 9ad5a19..73a76e8 100644 --- a/core/java/android/content/IContentService.aidl +++ b/core/java/android/content/IContentService.aidl @@ -17,6 +17,7 @@ package android.content; import android.accounts.Account; +import android.content.ComponentName; import android.content.SyncInfo; import android.content.ISyncStatusObserver; import android.content.SyncAdapterType; @@ -55,8 +56,14 @@ interface IContentService { int userHandle); void requestSync(in Account account, String authority, in Bundle extras); + /** + * Start a sync given a request. + */ void sync(in SyncRequest request); - void cancelSync(in Account account, String authority); + void cancelSync(in Account account, String authority, in ComponentName cname); + + /** Cancel a sync, providing information about the sync to be cancelled. */ + void cancelRequest(in SyncRequest request); /** * Check if the provider should be synced when a network tickle is received @@ -74,12 +81,14 @@ interface IContentService { void setSyncAutomatically(in Account account, String providerName, boolean sync); /** - * Get the frequency of the periodic poll, if any. - * @param providerName the provider whose setting we are querying - * @return the frequency of the periodic sync in seconds. If 0 then no periodic syncs - * will take place. + * Get a list of periodic operations for a specified authority, or service. + * @param account account for authority, must be null if cname is non-null. + * @param providerName name of provider, must be null if cname is non-null. + * @param cname component to identify sync service, must be null if account/providerName are + * non-null. */ - List<PeriodicSync> getPeriodicSyncs(in Account account, String providerName); + List<PeriodicSync> getPeriodicSyncs(in Account account, String providerName, + in ComponentName cname); /** * Set whether or not the provider is to be synced on a periodic basis. @@ -112,15 +121,22 @@ interface IContentService { */ void setIsSyncable(in Account account, String providerName, int syncable); - void setMasterSyncAutomatically(boolean flag); - - boolean getMasterSyncAutomatically(); + /** + * Corresponds roughly to setIsSyncable(String account, String provider) for syncs that bind + * to a SyncService. + */ + void setServiceActive(in ComponentName cname, boolean active); /** - * Returns true if there is currently a sync operation for the given - * account or authority in the pending list, or actively being processed. + * Corresponds roughly to getIsSyncable(String account, String provider) for syncs that bind + * to a SyncService. + * @return 0 if this SyncService is not enabled, 1 if enabled, <0 if unknown. */ - boolean isSyncActive(in Account account, String authority); + boolean isServiceActive(in ComponentName cname); + + void setMasterSyncAutomatically(boolean flag); + + boolean getMasterSyncAutomatically(); List<SyncInfo> getCurrentSyncs(); @@ -131,17 +147,33 @@ interface IContentService { SyncAdapterType[] getSyncAdapterTypes(); /** + * Returns true if there is currently a operation for the given account/authority or service + * actively being processed. + * @param account account for authority, must be null if cname is non-null. + * @param providerName name of provider, must be null if cname is non-null. + * @param cname component to identify sync service, must be null if account/providerName are + * non-null. + */ + boolean isSyncActive(in Account account, String authority, in ComponentName cname); + + /** * Returns the status that matches the authority. If there are multiples accounts for * the authority, the one with the latest "lastSuccessTime" status is returned. - * @param authority the authority whose row should be selected - * @return the SyncStatusInfo for the authority, or null if none exists + * @param account account for authority, must be null if cname is non-null. + * @param providerName name of provider, must be null if cname is non-null. + * @param cname component to identify sync service, must be null if account/providerName are + * non-null. */ - SyncStatusInfo getSyncStatus(in Account account, String authority); + SyncStatusInfo getSyncStatus(in Account account, String authority, in ComponentName cname); /** * Return true if the pending status is true of any matching authorities. + * @param account account for authority, must be null if cname is non-null. + * @param providerName name of provider, must be null if cname is non-null. + * @param cname component to identify sync service, must be null if account/providerName are + * non-null. */ - boolean isSyncPending(in Account account, String authority); + boolean isSyncPending(in Account account, String authority, in ComponentName cname); void addStatusChangeListener(int mask, ISyncStatusObserver callback); diff --git a/core/java/android/content/IAnonymousSyncAdapter.aidl b/core/java/android/content/ISyncServiceAdapter.aidl index a80cea3..d419307 100644 --- a/core/java/android/content/IAnonymousSyncAdapter.aidl +++ b/core/java/android/content/ISyncServiceAdapter.aidl @@ -24,7 +24,7 @@ import android.content.ISyncContext; * Provider specified). See {@link android.content.AbstractThreadedSyncAdapter}. * {@hide} */ -oneway interface IAnonymousSyncAdapter { +oneway interface ISyncServiceAdapter { /** * Initiate a sync. SyncAdapter-specific parameters may be specified in diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 95e27e2..a7d5606 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -21,6 +21,7 @@ import android.util.ArraySet; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; +import android.annotation.IntDef; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.content.pm.ActivityInfo; @@ -45,6 +46,8 @@ import com.android.internal.util.XmlUtils; import java.io.IOException; import java.io.Serializable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; @@ -2193,6 +2196,11 @@ public class Intent implements Parcelable, Cloneable { /** * Broadcast Action: Wired Headset plugged in or unplugged. * + * You <em>cannot</em> receive this through components declared + * in manifests, only by explicitly registering for it with + * {@link Context#registerReceiver(BroadcastReceiver, IntentFilter) + * Context.registerReceiver()}. + * * <p>The intent will have the following extra values: * <ul> * <li><em>state</em> - 0 for unplugged, 1 for plugged. </li> @@ -2623,6 +2631,23 @@ public class Intent implements Parcelable, Cloneable { "android.intent.action.USER_INFO_CHANGED"; /** + * Broadcast sent to the primary user when an associated managed profile is added (the profile + * was created and is ready to be used). Carries an extra {@link #EXTRA_USER} that specifies + * the UserHandle of the profile that was added. This is only sent to registered receivers, + * not manifest receivers. + */ + public static final String ACTION_MANAGED_PROFILE_ADDED = + "android.intent.action.MANAGED_PROFILE_ADDED"; + + /** + * Broadcast sent to the primary user when an associated managed profile is removed. Carries an + * extra {@link #EXTRA_USER} that specifies the UserHandle of the profile that was removed. This + * is only sent to registered receivers, not manifest receivers. + */ + public static final String ACTION_MANAGED_PROFILE_REMOVED = + "android.intent.action.MANAGED_PROFILE_REMOVED"; + + /** * Sent when the user taps on the clock widget in the system's "quick settings" area. */ public static final String ACTION_QUICK_CLOCK = @@ -2899,6 +2924,14 @@ public class Intent implements Parcelable, Cloneable { @SdkConstant(SdkConstantType.INTENT_CATEGORY) public static final String CATEGORY_CAR_MODE = "android.intent.category.CAR_MODE"; + /** + * An activity that provides a user interface for adjusting notification preferences for its + * containing application. Optional but recommended for apps that post + * {@link android.app.Notification Notifications}. + */ + @SdkConstant(SdkConstantType.INTENT_CATEGORY) + public static final String CATEGORY_NOTIFICATION_PREFERENCES = "android.intent.category.NOTIFICATION_PREFERENCES"; + // --------------------------------------------------------------------- // --------------------------------------------------------------------- // Application launch intent categories (see addCategory()). @@ -3308,15 +3341,23 @@ public class Intent implements Parcelable, Cloneable { "android.intent.extra.ALLOW_MULTIPLE"; /** - * The userHandle carried with broadcast intents related to addition, removal and switching of - * users - * - {@link #ACTION_USER_ADDED}, {@link #ACTION_USER_REMOVED} and {@link #ACTION_USER_SWITCHED}. + * The integer userHandle carried with broadcast intents related to addition, removal and + * switching of users and managed profiles - {@link #ACTION_USER_ADDED}, + * {@link #ACTION_USER_REMOVED} and {@link #ACTION_USER_SWITCHED}. + * * @hide */ public static final String EXTRA_USER_HANDLE = "android.intent.extra.user_handle"; /** + * The UserHandle carried with broadcasts intents related to addition and removal of managed + * profiles - {@link #ACTION_MANAGED_PROFILE_ADDED} and {@link #ACTION_MANAGED_PROFILE_REMOVED}. + */ + public static final String EXTRA_USER = + "android.intent.extra.user"; + + /** * Extra used in the response from a BroadcastReceiver that handles * {@link #ACTION_GET_RESTRICTION_ENTRIES}. The type of the extra is * <code>ArrayList<RestrictionEntry></code>. @@ -3373,6 +3414,12 @@ public class Intent implements Parcelable, Cloneable { // --------------------------------------------------------------------- // Intent flags (see mFlags variable). + /** @hide */ + @IntDef(flag = true, + value = {FLAG_GRANT_READ_URI_PERMISSION, FLAG_GRANT_WRITE_URI_PERMISSION}) + @Retention(RetentionPolicy.SOURCE) + public @interface GrantUriMode {} + /** * If set, the recipient of this Intent will be granted permission to * perform read operations on the URI in the Intent's data and any URIs @@ -3471,7 +3518,16 @@ public class Intent implements Parcelable, Cloneable { */ public static final int FLAG_ACTIVITY_NEW_TASK = 0x10000000; /** - * <strong>Do not use this flag unless you are implementing your own + * This flag is used to create a new task and launch an activity into it. + * This flag is always paired with either {@link #FLAG_ACTIVITY_NEW_DOCUMENT} + * or {@link #FLAG_ACTIVITY_NEW_TASK}. In both cases these flags alone would + * search through existing tasks for ones matching this Intent. Only if no such + * task is found would a new task be created. When paired with + * FLAG_ACTIVITY_MULTIPLE_TASK both of these behaviors are modified to skip + * the search for a matching task and unconditionally start a new task. + * + * <strong>When used with {@link #FLAG_ACTIVITY_NEW_TASK} do not use this + * flag unless you are implementing your own * top-level application launcher.</strong> Used in conjunction with * {@link #FLAG_ACTIVITY_NEW_TASK} to disable the * behavior of bringing an existing task to the foreground. When set, @@ -3483,12 +3539,18 @@ public class Intent implements Parcelable, Cloneable { * you should not use this flag unless you provide some way for a user to * return back to the tasks you have launched.</strong> * - * <p>This flag is ignored if - * {@link #FLAG_ACTIVITY_NEW_TASK} is not set. + * See {@link #FLAG_ACTIVITY_NEW_DOCUMENT} for details of this flag's use for + * creating new document tasks. + * + * <p>This flag is ignored if one of {@link #FLAG_ACTIVITY_NEW_TASK} or + * {@link #FLAG_ACTIVITY_NEW_TASK} is not also set. * * <p>See * <a href="{@docRoot}guide/topics/fundamentals/tasks-and-back-stack.html">Tasks and Back * Stack</a> for more information about tasks. + * + * @see #FLAG_ACTIVITY_NEW_DOCUMENT + * @see #FLAG_ACTIVITY_NEW_TASK */ public static final int FLAG_ACTIVITY_MULTIPLE_TASK = 0x08000000; /** @@ -3595,6 +3657,34 @@ public class Intent implements Parcelable, Cloneable { */ public static final int FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET = 0x00080000; /** + * This flag is used to break out "documents" into separate tasks that can + * be reached via the Recents mechanism. Such a document is any kind of + * item for which an application may want to maintain multiple simultaneous + * instances. Examples might be text files, web pages, spreadsheets, or + * emails. Each such document will be in a separate task in the Recents list. + * + * <p>When set, the activity specified by this Intent will launch into a + * separate task rooted at that activity. The activity launched must be + * defined with {@link android.R.attr#launchMode} "standard" or "singleTop". + * + * <p>If FLAG_ACTIVITY_NEW_DOCUMENT is used without + * {@link #FLAG_ACTIVITY_MULTIPLE_TASK} then the activity manager will + * search for an existing task with a matching target activity and Intent + * data URI and relaunch that task, first finishing all activities down to + * the root activity and then calling the root activity's + * {@link android.app.Activity#onNewIntent(Intent)} method. If no existing + * task's root activity matches the Intent's data URI then a new task will + * be launched with the target activity as root. + * + * <p>When paired with {@link #FLAG_ACTIVITY_MULTIPLE_TASK} this will + * always create a new task. Thus the same document may be made to appear + * more than one time in Recents. + * + * @see #FLAG_ACTIVITY_MULTIPLE_TASK + */ + public static final int FLAG_ACTIVITY_NEW_DOCUMENT = + FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET | FLAG_ACTIVITY_NEW_TASK; + /** * If set, this flag will prevent the normal {@link android.app.Activity#onUserLeaveHint} * callback from occurring on the current frontmost activity before it is * paused as the newly-started activity is brought to the front. @@ -6260,6 +6350,7 @@ public class Intent implements Parcelable, Cloneable { * @see #FLAG_ACTIVITY_FORWARD_RESULT * @see #FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY * @see #FLAG_ACTIVITY_MULTIPLE_TASK + * @see #FLAG_ACTIVITY_NEW_DOCUMENT * @see #FLAG_ACTIVITY_NEW_TASK * @see #FLAG_ACTIVITY_NO_ANIMATION * @see #FLAG_ACTIVITY_NO_HISTORY @@ -6417,6 +6508,21 @@ public class Intent implements Parcelable, Cloneable { } } + /** @hide */ + @IntDef(flag = true, + value = { + FILL_IN_ACTION, + FILL_IN_DATA, + FILL_IN_CATEGORIES, + FILL_IN_COMPONENT, + FILL_IN_PACKAGE, + FILL_IN_SOURCE_BOUNDS, + FILL_IN_SELECTOR, + FILL_IN_CLIP_DATA + }) + @Retention(RetentionPolicy.SOURCE) + public @interface FillInFlags {} + /** * Use with {@link #fillIn} to allow the current action value to be * overwritten, even if it is already set. @@ -6510,10 +6616,12 @@ public class Intent implements Parcelable, Cloneable { * * @return Returns a bit mask of {@link #FILL_IN_ACTION}, * {@link #FILL_IN_DATA}, {@link #FILL_IN_CATEGORIES}, {@link #FILL_IN_PACKAGE}, - * {@link #FILL_IN_COMPONENT}, {@link #FILL_IN_SOURCE_BOUNDS}, and - * {@link #FILL_IN_SELECTOR} indicating which fields were changed. + * {@link #FILL_IN_COMPONENT}, {@link #FILL_IN_SOURCE_BOUNDS}, + * {@link #FILL_IN_SELECTOR} and {@link #FILL_IN_CLIP_DATA indicating which fields were + * changed. */ - public int fillIn(Intent other, int flags) { + @FillInFlags + public int fillIn(Intent other, @FillInFlags int flags) { int changes = 0; if (other.mAction != null && (mAction == null || (flags&FILL_IN_ACTION) != 0)) { @@ -7339,4 +7447,9 @@ public class Intent implements Parcelable, Cloneable { String htmlText = htmlTexts != null ? htmlTexts.get(which) : null; return new ClipData.Item(text, htmlText, null, uri); } + + /** @hide */ + public boolean isDocument() { + return (mFlags & FLAG_ACTIVITY_NEW_DOCUMENT) == FLAG_ACTIVITY_NEW_DOCUMENT; + } } diff --git a/core/java/android/content/Loader.java b/core/java/android/content/Loader.java index a045b3a..e9d82af 100644 --- a/core/java/android/content/Loader.java +++ b/core/java/android/content/Loader.java @@ -413,7 +413,7 @@ public class Loader<D> { * {@link #onReset()} happens. You can retrieve the current abandoned * state with {@link #isAbandoned}. */ - protected void onAbandon() { + protected void onAbandon() { } /** diff --git a/core/java/android/content/PeriodicSync.java b/core/java/android/content/PeriodicSync.java index b586eec..836c6f8 100644 --- a/core/java/android/content/PeriodicSync.java +++ b/core/java/android/content/PeriodicSync.java @@ -29,13 +29,17 @@ public class PeriodicSync implements Parcelable { public final Account account; /** The authority of the sync. Can be null. */ public final String authority; + /** The service for syncing, if this is an anonymous sync. Can be null.*/ + public final ComponentName service; /** Any extras that parameters that are to be passed to the sync adapter. */ public final Bundle extras; /** How frequently the sync should be scheduled, in seconds. Kept around for API purposes. */ public final long period; + /** Whether this periodic sync runs on a {@link SyncService}. */ + public final boolean isService; /** - * {@hide} * How much flexibility can be taken in scheduling the sync, in seconds. + * {@hide} */ public final long flexTime; @@ -48,44 +52,74 @@ public class PeriodicSync implements Parcelable { public PeriodicSync(Account account, String authority, Bundle extras, long periodInSeconds) { this.account = account; this.authority = authority; + this.service = null; + this.isService = false; if (extras == null) { this.extras = new Bundle(); } else { this.extras = new Bundle(extras); } this.period = periodInSeconds; - // Initialise to a sane value. + // Old API uses default flex time. No-one should be using this ctor anyway. this.flexTime = 0L; } /** - * {@hide} * Create a copy of a periodic sync. + * {@hide} */ public PeriodicSync(PeriodicSync other) { this.account = other.account; this.authority = other.authority; + this.service = other.service; + this.isService = other.isService; this.extras = new Bundle(other.extras); this.period = other.period; this.flexTime = other.flexTime; } /** - * {@hide} * A PeriodicSync for a sync with a specified provider. + * {@hide} */ public PeriodicSync(Account account, String authority, Bundle extras, long period, long flexTime) { this.account = account; this.authority = authority; + this.service = null; + this.isService = false; + this.extras = new Bundle(extras); + this.period = period; + this.flexTime = flexTime; + } + + /** + * A PeriodicSync for a sync with a specified SyncService. + * {@hide} + */ + public PeriodicSync(ComponentName service, Bundle extras, + long period, + long flexTime) { + this.account = null; + this.authority = null; + this.service = service; + this.isService = true; this.extras = new Bundle(extras); this.period = period; this.flexTime = flexTime; } private PeriodicSync(Parcel in) { - this.account = in.readParcelable(null); - this.authority = in.readString(); + this.isService = (in.readInt() != 0); + if (this.isService) { + this.service = in.readParcelable(null); + this.account = null; + this.authority = null; + } else { + this.account = in.readParcelable(null); + this.authority = in.readString(); + this.service = null; + } this.extras = in.readBundle(); this.period = in.readLong(); this.flexTime = in.readLong(); @@ -98,8 +132,13 @@ public class PeriodicSync implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeParcelable(account, flags); - dest.writeString(authority); + dest.writeInt(isService ? 1 : 0); + if (account == null && authority == null) { + dest.writeParcelable(service, flags); + } else { + dest.writeParcelable(account, flags); + dest.writeString(authority); + } dest.writeBundle(extras); dest.writeLong(period); dest.writeLong(flexTime); @@ -126,14 +165,24 @@ public class PeriodicSync implements Parcelable { return false; } final PeriodicSync other = (PeriodicSync) o; - return account.equals(other.account) - && authority.equals(other.authority) + if (this.isService != other.isService) { + return false; + } + boolean equal = false; + if (this.isService) { + equal = service.equals(other.service); + } else { + equal = account.equals(other.account) + && authority.equals(other.authority); + } + return equal && period == other.period && syncExtrasEquals(extras, other.extras); } /** - * Periodic sync extra comparison function. + * Periodic sync extra comparison function. Duplicated from + * {@link com.android.server.content.SyncManager#syncExtrasEquals(Bundle b1, Bundle b2)} * {@hide} */ public static boolean syncExtrasEquals(Bundle b1, Bundle b2) { @@ -158,6 +207,7 @@ public class PeriodicSync implements Parcelable { public String toString() { return "account: " + account + ", authority: " + authority + + ", service: " + service + ". period: " + period + "s " + ", flex: " + flexTime; } diff --git a/core/java/android/content/RestrictionEntry.java b/core/java/android/content/RestrictionEntry.java index 283a097..3ff53bf 100644 --- a/core/java/android/content/RestrictionEntry.java +++ b/core/java/android/content/RestrictionEntry.java @@ -19,8 +19,6 @@ package android.content; import android.os.Parcel; import android.os.Parcelable; -import java.lang.annotation.Inherited; - /** * Applications can expose restrictions for a restricted user on a * multiuser device. The administrator can configure these restrictions that will then be diff --git a/core/java/android/content/SyncActivityTooManyDeletes.java b/core/java/android/content/SyncActivityTooManyDeletes.java index 350f35e..093fb08 100644 --- a/core/java/android/content/SyncActivityTooManyDeletes.java +++ b/core/java/android/content/SyncActivityTooManyDeletes.java @@ -95,7 +95,7 @@ public class SyncActivityTooManyDeletes extends Activity // try { // final Context authContext = createPackageContext(desc.packageName, 0); // ImageView imageView = new ImageView(this); -// imageView.setImageDrawable(authContext.getResources().getDrawable(desc.iconId)); +// imageView.setImageDrawable(authContext.getDrawable(desc.iconId)); // ll.addView(imageView, lp); // } catch (PackageManager.NameNotFoundException e) { // } diff --git a/core/java/android/content/SyncInfo.java b/core/java/android/content/SyncInfo.java index cffc653..146dd99 100644 --- a/core/java/android/content/SyncInfo.java +++ b/core/java/android/content/SyncInfo.java @@ -19,7 +19,6 @@ package android.content; import android.accounts.Account; import android.os.Parcel; import android.os.Parcelable; -import android.os.Parcelable.Creator; /** * Information about the sync operation that is currently underway. @@ -29,16 +28,24 @@ public class SyncInfo implements Parcelable { public final int authorityId; /** - * The {@link Account} that is currently being synced. + * The {@link Account} that is currently being synced. Will be null if this sync is running via + * a {@link SyncService}. */ public final Account account; /** - * The authority of the provider that is currently being synced. + * The authority of the provider that is currently being synced. Will be null if this sync + * is running via a {@link SyncService}. */ public final String authority; /** + * The {@link SyncService} that is targeted by this operation. Null if this sync is running via + * a {@link AbstractThreadedSyncAdapter}. + */ + public final ComponentName service; + + /** * The start time of the current sync operation in milliseconds since boot. * This is represented in elapsed real time. * See {@link android.os.SystemClock#elapsedRealtime()}. @@ -46,12 +53,13 @@ public class SyncInfo implements Parcelable { public final long startTime; /** @hide */ - public SyncInfo(int authorityId, Account account, String authority, + public SyncInfo(int authorityId, Account account, String authority, ComponentName service, long startTime) { this.authorityId = authorityId; this.account = account; this.authority = authority; this.startTime = startTime; + this.service = service; } /** @hide */ @@ -60,6 +68,7 @@ public class SyncInfo implements Parcelable { this.account = new Account(other.account.name, other.account.type); this.authority = other.authority; this.startTime = other.startTime; + this.service = other.service; } /** @hide */ @@ -70,17 +79,20 @@ public class SyncInfo implements Parcelable { /** @hide */ public void writeToParcel(Parcel parcel, int flags) { parcel.writeInt(authorityId); - account.writeToParcel(parcel, 0); + parcel.writeParcelable(account, flags); parcel.writeString(authority); parcel.writeLong(startTime); + parcel.writeParcelable(service, flags); + } /** @hide */ SyncInfo(Parcel parcel) { authorityId = parcel.readInt(); - account = new Account(parcel); + account = parcel.readParcelable(Account.class.getClassLoader()); authority = parcel.readString(); startTime = parcel.readLong(); + service = parcel.readParcelable(ComponentName.class.getClassLoader()); } /** @hide */ diff --git a/core/java/android/content/SyncRequest.java b/core/java/android/content/SyncRequest.java index 6ca283d..a9a62a7 100644 --- a/core/java/android/content/SyncRequest.java +++ b/core/java/android/content/SyncRequest.java @@ -23,15 +23,15 @@ import android.os.Parcelable; public class SyncRequest implements Parcelable { private static final String TAG = "SyncRequest"; - /** Account to pass to the sync adapter. May be null. */ + /** Account to pass to the sync adapter. Can be null. */ private final Account mAccountToSync; /** Authority string that corresponds to a ContentProvider. */ private final String mAuthority; - /** Sync service identifier. May be null.*/ + /** {@link SyncService} identifier. */ private final ComponentName mComponentInfo; /** Bundle containing user info as well as sync settings. */ private final Bundle mExtras; - /** Disallow this sync request on metered networks. */ + /** Don't allow this sync request on metered networks. */ private final boolean mDisallowMetered; /** * Anticipated upload size in bytes. @@ -69,18 +69,14 @@ public class SyncRequest implements Parcelable { return mIsPeriodic; } - /** - * {@hide} - * @return whether this is an expedited sync. - */ public boolean isExpedited() { return mIsExpedited; } /** * {@hide} - * @return true if this sync uses an account/authority pair, or false if this sync is bound to - * a Sync Service. + * @return true if this sync uses an account/authority pair, or false if + * this is an anonymous sync bound to an @link AnonymousSyncService. */ public boolean hasAuthority() { return mIsAuthority; @@ -88,34 +84,51 @@ public class SyncRequest implements Parcelable { /** * {@hide} + * * @return account object for this sync. - * @throws IllegalArgumentException if this function is called for a request that does not - * specify an account/provider authority. + * @throws IllegalArgumentException if this function is called for a request that targets a + * sync service. */ public Account getAccount() { if (!hasAuthority()) { - throw new IllegalArgumentException("Cannot getAccount() for a sync that does not" - + "specify an authority."); + throw new IllegalArgumentException("Cannot getAccount() for a sync that targets a sync" + + "service."); } return mAccountToSync; } /** * {@hide} + * * @return provider for this sync. - * @throws IllegalArgumentException if this function is called for a request that does not - * specify an account/provider authority. + * @throws IllegalArgumentException if this function is called for a request that targets a + * sync service. */ public String getProvider() { if (!hasAuthority()) { - throw new IllegalArgumentException("Cannot getProvider() for a sync that does not" - + "specify a provider."); + throw new IllegalArgumentException("Cannot getProvider() for a sync that targets a" + + "sync service."); } return mAuthority; } /** * {@hide} + * Throws a runtime IllegalArgumentException if this function is called for a + * SyncRequest that is bound to an account/provider. + * + * @return ComponentName for the service that this sync will bind to. + */ + public ComponentName getService() { + if (hasAuthority()) { + throw new IllegalArgumentException( + "Cannot getAnonymousService() for a sync that has specified a provider."); + } + return mComponentInfo; + } + + /** + * {@hide} * Retrieve bundle for this SyncRequest. Will not be null. */ public Bundle getBundle() { @@ -129,7 +142,6 @@ public class SyncRequest implements Parcelable { public long getSyncFlexTime() { return mSyncFlexTimeSecs; } - /** * {@hide} * @return the last point in time at which this sync must scheduled. @@ -216,7 +228,7 @@ public class SyncRequest implements Parcelable { } /** - * Builder class for a {@link SyncRequest}. As you build your SyncRequest this class will also + * Builder class for a @link SyncRequest. As you build your SyncRequest this class will also * perform validation. */ public static class Builder { @@ -232,9 +244,12 @@ public class SyncRequest implements Parcelable { private static final int SYNC_TARGET_SERVICE = 1; /** Specify that this is a sync with a provider. */ private static final int SYNC_TARGET_ADAPTER = 2; - /** Earliest point of displacement into the future at which this sync can occur. */ + /** + * Earliest point of displacement into the future at which this sync can + * occur. + */ private long mSyncFlexTimeSecs; - /** Latest point of displacement into the future at which this sync must occur. */ + /** Displacement into the future at which this sync must occur. */ private long mSyncRunTimeSecs; /** * Sync configuration information - custom user data explicitly provided by the developer. @@ -283,8 +298,9 @@ public class SyncRequest implements Parcelable { private boolean mExpedited; /** - * The sync component that contains the sync logic if this is a provider-less sync, - * otherwise null. + * The {@link SyncService} component that + * contains the sync logic if this is a provider-less sync, otherwise + * null. */ private ComponentName mComponentName; /** @@ -320,11 +336,15 @@ public class SyncRequest implements Parcelable { /** * Build a periodic sync. Either this or syncOnce() <b>must</b> be called for this builder. - * Syncs are identified by target {@link android.provider}/{@link android.accounts.Account} - * and by the contents of the extras bundle. - * You cannot reuse the same builder for one-time syncs (by calling this function) after - * having specified a periodic sync. If you do, an <code>IllegalArgumentException</code> + * Syncs are identified by target {@link SyncService}/{@link android.provider} and by the + * contents of the extras bundle. + * You cannot reuse the same builder for one-time syncs after having specified a periodic + * sync (by calling this function). If you do, an <code>IllegalArgumentException</code> * will be thrown. + * <p>The bundle for a periodic sync can be queried by applications with the correct + * permissions using + * {@link ContentResolver#getPeriodicSyncs(Account account, String provider)}, so no + * sensitive data should be transferred here. * * Example usage. * @@ -375,7 +395,6 @@ public class SyncRequest implements Parcelable { } /** - * {@hide} * Developer can provide insight into their payload size; optional. -1 specifies unknown, * so that you are not restricted to defining both fields. * @@ -389,20 +408,28 @@ public class SyncRequest implements Parcelable { } /** - * @see android.net.ConnectivityManager#isActiveNetworkMetered() - * @param disallow true to enforce that this transfer not occur on metered networks. - * Default false. + * Will throw an <code>IllegalArgumentException</code> if called and + * {@link #setIgnoreSettings(boolean ignoreSettings)} has already been called. + * @param disallow true to allow this transfer on metered networks. Default false. + * */ public Builder setDisallowMetered(boolean disallow) { + if (mIgnoreSettings && disallow) { + throw new IllegalArgumentException("setDisallowMetered(true) after having" + + "specified that settings are ignored."); + } mDisallowMetered = disallow; return this; } /** - * Specify an authority and account for this transfer. + * Specify an authority and account for this transfer. Cannot be used with + * {@link #setSyncAdapter(ComponentName cname)}. * - * @param authority String identifying which content provider to sync. - * @param account Account to sync. Can be null unless this is a periodic sync. + * @param authority + * @param account Account to sync. Can be null unless this is a periodic + * sync, for which verification by the ContentResolver will + * fail. If a sync is performed without an account, the */ public Builder setSyncAdapter(Account account, String authority) { if (mSyncTarget != SYNC_TARGET_UNKNOWN) { @@ -419,10 +446,26 @@ public class SyncRequest implements Parcelable { } /** - * Optional developer-provided extras handed back in - * {@link AbstractThreadedSyncAdapter#onPerformSync(Account, Bundle, String, - * ContentProviderClient, SyncResult)} occurs. This bundle is copied into the SyncRequest - * returned by {@link #build()}. + * Specify the {@link SyncService} component for this sync. This is not validated until + * sync time so providing an incorrect component name here will not fail. Cannot be used + * with {@link #setSyncAdapter(Account account, String authority)}. + * + * @param cname ComponentName to identify your Anonymous service + */ + public Builder setSyncAdapter(ComponentName cname) { + if (mSyncTarget != SYNC_TARGET_UNKNOWN) { + throw new IllegalArgumentException("Sync target has already been defined."); + } + mSyncTarget = SYNC_TARGET_SERVICE; + mComponentName = cname; + mAccount = null; + mAuthority = null; + return this; + } + + /** + * Developer-provided extras handed back when sync actually occurs. This bundle is copied + * into the SyncRequest returned by {@link #build()}. * * Example: * <pre> @@ -436,7 +479,7 @@ public class SyncRequest implements Parcelable { * Bundle extras = new Bundle(); * extras.setString("data", syncData); * builder.setExtras(extras); - * ContentResolver.sync(builder.build()); // Each sync() request is for a unique sync. + * ContentResolver.sync(builder.build()); // Each sync() request creates a unique sync. * } * </pre> * Only values of the following types may be used in the extras bundle: @@ -477,13 +520,19 @@ public class SyncRequest implements Parcelable { /** * Convenience function for setting {@link ContentResolver#SYNC_EXTRAS_IGNORE_SETTINGS}. * - * A sync can specify that system sync settings be ignored (user has turned sync off). Not - * valid for periodic sync and will throw an <code>IllegalArgumentException</code> in + * Not valid for periodic sync and will throw an <code>IllegalArgumentException</code> in * {@link #build()}. + * <p>Throws <code>IllegalArgumentException</code> if called and + * {@link #setDisallowMetered(boolean)} has been set. + * * * @param ignoreSettings true to ignore the sync automatically settings. Default false. */ public Builder setIgnoreSettings(boolean ignoreSettings) { + if (mDisallowMetered && ignoreSettings) { + throw new IllegalArgumentException("setIgnoreSettings(true) after having specified" + + " sync settings with this builder."); + } mIgnoreSettings = ignoreSettings; return this; } @@ -491,13 +540,13 @@ public class SyncRequest implements Parcelable { /** * Convenience function for setting {@link ContentResolver#SYNC_EXTRAS_IGNORE_BACKOFF}. * - * Force the sync scheduling process to ignore any back-off that was the result of a failed - * sync, as well as to invalidate any {@link SyncResult#delayUntil} value that may have - * been set by the adapter. Successive failures will not honor this flag. Not valid for - * periodic sync and will throw an <code>IllegalArgumentException</code> in - * {@link #build()}. + * Ignoring back-off will force the sync scheduling process to ignore any back-off that was + * the result of a failed sync, as well as to invalidate any {@link SyncResult#delayUntil} + * value that may have been set by the adapter. Successive failures will not honor this + * flag. Not valid for periodic sync and will throw an <code>IllegalArgumentException</code> + * in {@link #build()}. * - * @param ignoreBackoff ignore back-off settings. Default false. + * @param ignoreBackoff ignore back off settings. Default false. */ public Builder setIgnoreBackoff(boolean ignoreBackoff) { mIgnoreBackoff = ignoreBackoff; @@ -507,9 +556,8 @@ public class SyncRequest implements Parcelable { /** * Convenience function for setting {@link ContentResolver#SYNC_EXTRAS_MANUAL}. * - * A manual sync is functionally equivalent to calling {@link #setIgnoreBackoff(boolean)} - * and {@link #setIgnoreSettings(boolean)}. Not valid for periodic sync and will throw an - * <code>IllegalArgumentException</code> in {@link #build()}. + * Not valid for periodic sync and will throw an <code>IllegalArgumentException</code> in + * {@link #build()}. * * @param isManual User-initiated sync or not. Default false. */ @@ -519,7 +567,7 @@ public class SyncRequest implements Parcelable { } /** - * An expedited sync runs immediately and will preempt another non-expedited running sync. + * An expedited sync runs immediately and can preempt other non-expedited running syncs. * * Not valid for periodic sync and will throw an <code>IllegalArgumentException</code> in * {@link #build()}. @@ -532,7 +580,6 @@ public class SyncRequest implements Parcelable { } /** - * {@hide} * @param priority the priority of this request among all requests from the calling app. * Range of [-2,2] similar to how this is done with notifications. */ @@ -552,11 +599,11 @@ public class SyncRequest implements Parcelable { * builder. */ public SyncRequest build() { + // Validate the extras bundle + ContentResolver.validateSyncExtrasBundle(mCustomExtras); if (mCustomExtras == null) { mCustomExtras = new Bundle(); } - // Validate the extras bundle - ContentResolver.validateSyncExtrasBundle(mCustomExtras); // Combine builder extra flags into the config bundle. mSyncConfigExtras = new Bundle(); if (mIgnoreBackoff) { @@ -575,51 +622,33 @@ public class SyncRequest implements Parcelable { mSyncConfigExtras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); } if (mIsManual) { - mSyncConfigExtras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); + mSyncConfigExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, true); + mSyncConfigExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true); } mSyncConfigExtras.putLong(ContentResolver.SYNC_EXTRAS_EXPECTED_UPLOAD, mTxBytes); mSyncConfigExtras.putLong(ContentResolver.SYNC_EXTRAS_EXPECTED_DOWNLOAD, mRxBytes); mSyncConfigExtras.putInt(ContentResolver.SYNC_EXTRAS_PRIORITY, mPriority); if (mSyncType == SYNC_TYPE_PERIODIC) { // If this is a periodic sync ensure than invalid extras were not set. - validatePeriodicExtras(mCustomExtras); - validatePeriodicExtras(mSyncConfigExtras); - // Verify that account and provider are not null. - if (mAccount == null) { - throw new IllegalArgumentException("Account must not be null for periodic" - + " sync."); - } - if (mAuthority == null) { - throw new IllegalArgumentException("Authority must not be null for periodic" - + " sync."); + if (ContentResolver.invalidPeriodicExtras(mCustomExtras) || + ContentResolver.invalidPeriodicExtras(mSyncConfigExtras)) { + throw new IllegalArgumentException("Illegal extras were set"); } } else if (mSyncType == SYNC_TYPE_UNKNOWN) { throw new IllegalArgumentException("Must call either syncOnce() or syncPeriodic()"); } + if (mSyncTarget == SYNC_TARGET_SERVICE) { + if (mSyncConfigExtras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false)) { + throw new IllegalArgumentException("Cannot specify an initialisation sync" + + " that targets a service."); + } + } // Ensure that a target for the sync has been set. if (mSyncTarget == SYNC_TARGET_UNKNOWN) { - throw new IllegalArgumentException("Must specify an adapter with " - + "setSyncAdapter(Account, String"); + throw new IllegalArgumentException("Must specify an adapter with one of" + + "setSyncAdapter(ComponentName) or setSyncAdapter(Account, String"); } return new SyncRequest(this); } - - /** - * Helper function to throw an <code>IllegalArgumentException</code> if any illegal - * extras were set for a periodic sync. - * - * @param extras bundle to validate. - */ - private void validatePeriodicExtras(Bundle extras) { - if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false) - || extras.getBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, false) - || extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false) - || extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, false) - || extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false) - || extras.getBoolean(ContentResolver.SYNC_EXTRAS_FORCE, false) - || extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false)) { - throw new IllegalArgumentException("Illegal extras were set"); - } - } - } + } } diff --git a/core/java/android/content/SyncService.java b/core/java/android/content/SyncService.java new file mode 100644 index 0000000..4df998c --- /dev/null +++ b/core/java/android/content/SyncService.java @@ -0,0 +1,211 @@ +/* + * 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.content; + +import android.app.Service; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Process; +import android.os.Trace; +import android.util.SparseArray; +import android.util.Log; + +import com.android.internal.annotations.GuardedBy; + +/** + * Simplified @link android.content.AbstractThreadedSyncAdapter. Folds that + * behaviour into a service to which the system can bind when requesting an + * anonymous (providerless/accountless) sync. + * <p> + * In order to perform an anonymous sync operation you must extend this service, implementing the + * abstract methods. This service must be declared in the application's manifest as usual. You + * can use this service for other work, however you <b> must not </b> override the onBind() method + * unless you know what you're doing, which limits the usefulness of this service for other work. + * <p>A {@link SyncService} can either be active or inactive. Different to an + * {@link AbstractThreadedSyncAdapter}, there is no + * {@link ContentResolver#setSyncAutomatically(android.accounts.Account account, String provider, boolean sync)}, + * as well as no concept of initialisation (you can handle your own if needed). + * + * <pre> + * <service android:name=".MySyncService"/> + * </pre> + * Like @link android.content.AbstractThreadedSyncAdapter this service supports + * multiple syncs at the same time. Each incoming startSync() with a unique tag + * will spawn a thread to do the work of that sync. If startSync() is called + * with a tag that already exists, a SyncResult.ALREADY_IN_PROGRESS is returned. + * Remember that your service will spawn multiple threads if you schedule multiple syncs + * at once, so if you mutate local objects you must ensure synchronization. + */ +public abstract class SyncService extends Service { + private static final String TAG = "SyncService"; + + private final SyncAdapterImpl mSyncAdapter = new SyncAdapterImpl(); + + /** Keep track of on-going syncs, keyed by bundle. */ + @GuardedBy("mSyncThreadLock") + private final SparseArray<SyncThread> + mSyncThreads = new SparseArray<SyncThread>(); + /** Lock object for accessing the SyncThreads HashMap. */ + private final Object mSyncThreadLock = new Object(); + /** + * Default key for if this sync service does not support parallel operations. Currently not + * sure if null keys will make it into the ArrayMap for KLP, so keeping our default for now. + */ + private static final int KEY_DEFAULT = 0; + /** Identifier for this sync service. */ + private ComponentName mServiceComponent; + + /** {@hide} */ + public IBinder onBind(Intent intent) { + mServiceComponent = new ComponentName(this, getClass()); + return mSyncAdapter.asBinder(); + } + + /** {@hide} */ + private class SyncAdapterImpl extends ISyncServiceAdapter.Stub { + @Override + public void startSync(ISyncContext syncContext, Bundle extras) { + // Wrap the provided Sync Context because it may go away by the time + // we call it. + final SyncContext syncContextClient = new SyncContext(syncContext); + boolean alreadyInProgress = false; + final int extrasAsKey = extrasToKey(extras); + synchronized (mSyncThreadLock) { + if (mSyncThreads.get(extrasAsKey) == null) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "starting sync for : " + mServiceComponent); + } + // Start sync. + SyncThread syncThread = new SyncThread(syncContextClient, extras); + mSyncThreads.put(extrasAsKey, syncThread); + syncThread.start(); + } else { + // Don't want to call back to SyncManager while still + // holding lock. + alreadyInProgress = true; + } + } + if (alreadyInProgress) { + syncContextClient.onFinished(SyncResult.ALREADY_IN_PROGRESS); + } + } + + /** + * Used by the SM to cancel a specific sync using the + * com.android.server.content.SyncManager.ActiveSyncContext as a handle. + */ + @Override + public void cancelSync(ISyncContext syncContext) { + SyncThread runningSync = null; + synchronized (mSyncThreadLock) { + for (int i = 0; i < mSyncThreads.size(); i++) { + SyncThread thread = mSyncThreads.valueAt(i); + if (thread.mSyncContext.getSyncContextBinder() == syncContext.asBinder()) { + runningSync = thread; + break; + } + } + } + if (runningSync != null) { + runningSync.interrupt(); + } + } + } + + /** + * + * @param extras Bundle for which to compute hash + * @return an integer hash that is equal to that of another bundle if they both contain the + * same key -> value mappings, however, not necessarily in order. + * Based on the toString() representation of the value mapped. + */ + private int extrasToKey(Bundle extras) { + int hash = KEY_DEFAULT; // Empty bundle, or no parallel operations enabled. + if (parallelSyncsEnabled()) { + for (String key : extras.keySet()) { + String mapping = key + " " + extras.get(key).toString(); + hash += mapping.hashCode(); + } + } + return hash; + } + + /** + * {@hide} + * Similar to {@link android.content.AbstractThreadedSyncAdapter.SyncThread}. However while + * the ATSA considers an already in-progress sync to be if the account provided is currently + * syncing, this anonymous sync has no notion of account and considers a sync unique if the + * provided bundle is different. + */ + private class SyncThread extends Thread { + private final SyncContext mSyncContext; + private final Bundle mExtras; + private final int mThreadsKey; + + public SyncThread(SyncContext syncContext, Bundle extras) { + mSyncContext = syncContext; + mExtras = extras; + mThreadsKey = extrasToKey(extras); + } + + @Override + public void run() { + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + + Trace.traceBegin(Trace.TRACE_TAG_SYNC_MANAGER, getApplication().getPackageName()); + + SyncResult syncResult = new SyncResult(); + try { + if (isCancelled()) return; + // Run the sync. + SyncService.this.onPerformSync(mExtras, syncResult); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_SYNC_MANAGER); + if (!isCancelled()) { + mSyncContext.onFinished(syncResult); + } + // Synchronize so that the assignment will be seen by other + // threads that also synchronize accesses to mSyncThreads. + synchronized (mSyncThreadLock) { + mSyncThreads.remove(mThreadsKey); + } + } + } + + private boolean isCancelled() { + return Thread.currentThread().isInterrupted(); + } + } + + /** + * Initiate an anonymous sync using this service. SyncAdapter-specific + * parameters may be specified in extras, which is guaranteed to not be + * null. + */ + public abstract void onPerformSync(Bundle extras, SyncResult syncResult); + + /** + * Override this function to indicated whether you want to support parallel syncs. + * <p>If you override and return true multiple threads will be spawned within your Service to + * handle each concurrent sync request. + * + * @return false to indicate that this service does not support parallel operations by default. + */ + protected boolean parallelSyncsEnabled() { + return false; + } +} diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index b8ac3bf..9916476 100644 --- a/core/java/android/content/pm/ActivityInfo.java +++ b/core/java/android/content/pm/ActivityInfo.java @@ -16,11 +16,15 @@ package android.content.pm; +import android.annotation.IntDef; import android.content.res.Configuration; import android.os.Parcel; import android.os.Parcelable; import android.util.Printer; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * Information you can retrieve about a particular application * activity or receiver. This corresponds to information collected @@ -185,6 +189,13 @@ public class ActivityInfo extends ComponentInfo */ public static final int FLAG_IMMERSIVE = 0x0800; /** + * Bit in {@link #flags} indicating that this activity is to be persisted across + * reboots for display in the Recents list. + * {@link android.R.attr#persistable} + * @hide + */ + public static final int FLAG_PERSISTABLE = 0x1000; + /** * @hide Bit in {@link #flags}: If set, this component will only be seen * by the primary user. Only works with broadcast receivers. Set from the * {@link android.R.attr#primaryUserOnly} attribute. @@ -212,6 +223,28 @@ public class ActivityInfo extends ComponentInfo */ public int flags; + /** @hide */ + @IntDef({ + SCREEN_ORIENTATION_UNSPECIFIED, + SCREEN_ORIENTATION_LANDSCAPE, + SCREEN_ORIENTATION_PORTRAIT, + SCREEN_ORIENTATION_USER, + SCREEN_ORIENTATION_BEHIND, + SCREEN_ORIENTATION_SENSOR, + SCREEN_ORIENTATION_NOSENSOR, + SCREEN_ORIENTATION_SENSOR_LANDSCAPE, + SCREEN_ORIENTATION_SENSOR_PORTRAIT, + SCREEN_ORIENTATION_REVERSE_LANDSCAPE, + SCREEN_ORIENTATION_REVERSE_PORTRAIT, + SCREEN_ORIENTATION_FULL_SENSOR, + SCREEN_ORIENTATION_USER_LANDSCAPE, + SCREEN_ORIENTATION_USER_PORTRAIT, + SCREEN_ORIENTATION_FULL_USER, + SCREEN_ORIENTATION_LOCKED + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ScreenOrientation {} + /** * Constant corresponding to <code>unspecified</code> in * the {@link android.R.attr#screenOrientation} attribute. @@ -323,6 +356,7 @@ public class ActivityInfo extends ComponentInfo * {@link #SCREEN_ORIENTATION_FULL_USER}, * {@link #SCREEN_ORIENTATION_LOCKED}, */ + @ScreenOrientation public int screenOrientation = SCREEN_ORIENTATION_UNSPECIFIED; /** diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java index 0172509..0d1b262 100644 --- a/core/java/android/content/pm/ApplicationInfo.java +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -344,7 +344,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { * the normal application lifecycle. * * <p>Comes from the - * {@link android.R.styleable#AndroidManifestApplication_cantSaveState android:cantSaveState} + * android.R.styleable#AndroidManifestApplication_cantSaveState * attribute of the <application> tag. * * {@hide} @@ -471,7 +471,13 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { * behavior was introduced. */ public int targetSdkVersion; - + + /** + * The app's declared version code. + * @hide + */ + public int versionCode; + /** * When false, indicates that all components within this application are * considered disabled, regardless of their individually set enabled status. @@ -523,7 +529,8 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { if (sharedLibraryFiles != null) { pw.println(prefix + "sharedLibraryFiles=" + sharedLibraryFiles); } - pw.println(prefix + "enabled=" + enabled + " targetSdkVersion=" + targetSdkVersion); + pw.println(prefix + "enabled=" + enabled + " targetSdkVersion=" + targetSdkVersion + + " versionCode=" + versionCode); if (manageSpaceActivityName != null) { pw.println(prefix + "manageSpaceActivityName="+manageSpaceActivityName); } @@ -592,6 +599,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { dataDir = orig.dataDir; uid = orig.uid; targetSdkVersion = orig.targetSdkVersion; + versionCode = orig.versionCode; enabled = orig.enabled; enabledSetting = orig.enabledSetting; installLocation = orig.installLocation; @@ -633,6 +641,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { dest.writeString(dataDir); dest.writeInt(uid); dest.writeInt(targetSdkVersion); + dest.writeInt(versionCode); dest.writeInt(enabled ? 1 : 0); dest.writeInt(enabledSetting); dest.writeInt(installLocation); @@ -673,6 +682,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { dataDir = source.readString(); uid = source.readInt(); targetSdkVersion = source.readInt(); + versionCode = source.readInt(); enabled = source.readInt() != 0; enabledSetting = source.readInt(); installLocation = source.readInt(); diff --git a/core/java/android/content/pm/FeatureInfo.java b/core/java/android/content/pm/FeatureInfo.java index 89394f9..d919fc3 100644 --- a/core/java/android/content/pm/FeatureInfo.java +++ b/core/java/android/content/pm/FeatureInfo.java @@ -18,7 +18,6 @@ package android.content.pm; import android.os.Parcel; import android.os.Parcelable; -import android.os.Parcelable.Creator; /** * A single feature that can be requested by an application. This corresponds diff --git a/core/java/android/content/pm/ILauncherApps.aidl b/core/java/android/content/pm/ILauncherApps.aidl new file mode 100644 index 0000000..796b113 --- /dev/null +++ b/core/java/android/content/pm/ILauncherApps.aidl @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2014, 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.content.pm; + +import android.content.ComponentName; +import android.content.Intent; +import android.content.pm.IOnAppsChangedListener; +import android.content.pm.ResolveInfo; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.UserHandle; +import java.util.List; + +/** + * {@hide} + */ +interface ILauncherApps { + void addOnAppsChangedListener(in IOnAppsChangedListener listener); + void removeOnAppsChangedListener(in IOnAppsChangedListener listener); + List<ResolveInfo> getLauncherActivities(String packageName, in UserHandle user); + ResolveInfo resolveActivity(in Intent intent, in UserHandle user); + void startActivityAsUser(in ComponentName component, in Rect sourceBounds, + in Bundle opts, in UserHandle user); +} diff --git a/core/java/android/content/pm/IOnAppsChangedListener.aidl b/core/java/android/content/pm/IOnAppsChangedListener.aidl new file mode 100644 index 0000000..796b58d --- /dev/null +++ b/core/java/android/content/pm/IOnAppsChangedListener.aidl @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2014, 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.content.pm; + +import android.os.UserHandle; + +/** + * {@hide} + */ +oneway interface IOnAppsChangedListener { + void onPackageRemoved(in UserHandle user, String packageName); + void onPackageAdded(in UserHandle user, String packageName); + void onPackageChanged(in UserHandle user, String packageName); + void onPackagesAvailable(in UserHandle user, in String[] packageNames, boolean replacing); + void onPackagesUnavailable(in UserHandle user, in String[] packageNames, boolean replacing); +} diff --git a/core/java/android/content/pm/IPackageInstallObserver2.aidl b/core/java/android/content/pm/IPackageInstallObserver2.aidl new file mode 100644 index 0000000..2602ab5 --- /dev/null +++ b/core/java/android/content/pm/IPackageInstallObserver2.aidl @@ -0,0 +1,45 @@ +/* +** +** Copyright 2014, 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.content.pm; + +import android.os.Bundle; + +/** + * API for installation callbacks from the Package Manager. In certain result cases + * additional information will be provided. + * @hide + */ +oneway interface IPackageInstallObserver2 { + /** + * The install operation has completed. {@code returnCode} holds a numeric code + * indicating success or failure. In certain cases the {@code extras} Bundle will + * contain additional details: + * + * <p><table> + * <tr> + * <td>INSTALL_FAILED_DUPLICATE_PERMISSION</td> + * <td>Two strings are provided in the extras bundle: EXTRA_EXISTING_PERMISSION + * is the name of the permission that the app is attempting to define, and + * EXTRA_EXISTING_PACKAGE is the package name of the app which has already + * defined the permission.</td> + * </tr> + * </table> + */ + void packageInstalled(in String packageName, in Bundle extras, int returnCode); +} + diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index 20002ad..ae0899f 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -25,6 +25,7 @@ import android.content.pm.ApplicationInfo; import android.content.pm.ContainerEncryptionParams; import android.content.pm.FeatureInfo; import android.content.pm.IPackageInstallObserver; +import android.content.pm.IPackageInstallObserver2; import android.content.pm.IPackageDeleteObserver; import android.content.pm.IPackageDataObserver; import android.content.pm.IPackageMoveObserver; @@ -237,6 +238,10 @@ interface IPackageManager { int getPreferredActivities(out List<IntentFilter> outFilters, out List<ComponentName> outActivities, String packageName); + void addPersistentPreferredActivity(in IntentFilter filter, in ComponentName activity, int userId); + + void clearPackagePersistentPreferredActivities(String packageName, int userId); + /** * Report the set of 'Home' activity candidates, plus (if any) which of them * is the current "always use this one" setting. @@ -402,6 +407,21 @@ interface IPackageManager { in VerificationParams verificationParams, in ContainerEncryptionParams encryptionParams); + /** Expanded observer versions */ + void installPackageEtc(in Uri packageURI, IPackageInstallObserver observer, + IPackageInstallObserver2 observer2, int flags, in String installerPackageName); + + void installPackageWithVerificationEtc(in Uri packageURI, + in IPackageInstallObserver observer, IPackageInstallObserver2 observer2, + int flags, in String installerPackageName, in Uri verificationURI, + in ManifestDigest manifestDigest, in ContainerEncryptionParams encryptionParams); + + void installPackageWithVerificationAndEncryptionEtc(in Uri packageURI, + in IPackageInstallObserver observer, in IPackageInstallObserver2 observer2, + int flags, in String installerPackageName, + in VerificationParams verificationParams, + in ContainerEncryptionParams encryptionParams); + int installExistingPackageAsUser(String packageName, int userId); void verifyPendingInstall(int id, int verificationCode); diff --git a/core/java/android/content/pm/LauncherActivityInfo.java b/core/java/android/content/pm/LauncherActivityInfo.java new file mode 100644 index 0000000..92b9146 --- /dev/null +++ b/core/java/android/content/pm/LauncherActivityInfo.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2014 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.content.pm; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Bitmap.Config; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.Log; + +/** + * A representation of an activity that can belong to this user or a managed + * profile associated with this user. It can be used to query the label, icon + * and badged icon for the activity. + */ +public class LauncherActivityInfo { + private static final boolean DEBUG = false; + private final PackageManager mPm; + private final UserManager mUm; + + private ActivityInfo mActivityInfo; + private ComponentName mComponentName; + private UserHandle mUser; + // TODO: Fetch this value from PM + private long mFirstInstallTime; + + /** + * Create a launchable activity object for a given ResolveInfo and user. + * + * @param context The context for fetching resources. + * @param info ResolveInfo from which to create the LauncherActivityInfo. + * @param user The UserHandle of the profile to which this activity belongs. + */ + LauncherActivityInfo(Context context, ResolveInfo info, UserHandle user) { + this(context); + this.mActivityInfo = info.activityInfo; + this.mComponentName = LauncherApps.getComponentName(info); + this.mUser = user; + } + + LauncherActivityInfo(Context context) { + mPm = context.getPackageManager(); + mUm = UserManager.get(context); + } + + /** + * Returns the component name of this activity. + * + * @return ComponentName of the activity + */ + public ComponentName getComponentName() { + return mComponentName; + } + + /** + * Returns the user handle of the user profile that this activity belongs to. + * + * @return The UserHandle of the profile. + */ + public UserHandle getUser() { + return mUser; + } + + /** + * Retrieves the label for the activity. + * + * @return The label for the activity. + */ + public CharSequence getLabel() { + return mActivityInfo.loadLabel(mPm); + } + + /** + * Returns the icon for this activity, without any badging for the profile. + * @param density The preferred density of the icon, zero for default density. + * @see #getBadgedIcon(int) + * @return The drawable associated with the activity + */ + public Drawable getIcon(int density) { + // TODO: Use density + return mActivityInfo.loadIcon(mPm); + } + + /** + * Returns the application flags from the ApplicationInfo of the activity. + * + * @return Application flags + */ + public int getApplicationFlags() { + return mActivityInfo.applicationInfo.flags; + } + + /** + * Returns the time at which the package was first installed. + * @return The time of installation of the package, in milliseconds. + */ + public long getFirstInstallTime() { + return mFirstInstallTime; + } + + /** + * Returns the activity icon with badging appropriate for the profile. + * @param density Optional density for the icon, or 0 to use the default density. + * @return A badged icon for the activity. + */ + public Drawable getBadgedIcon(int density) { + // TODO: Handle density + if (mUser.equals(android.os.Process.myUserHandle())) { + return mActivityInfo.loadIcon(mPm); + } + Drawable originalIcon = mActivityInfo.loadIcon(mPm); + if (originalIcon == null) { + if (DEBUG) { + Log.w("LauncherActivityInfo", "Couldn't find icon for activity"); + } + originalIcon = mPm.getDefaultActivityIcon(); + } + if (originalIcon instanceof BitmapDrawable) { + return mUm.getBadgedDrawableForUser( + originalIcon, mUser); + } + return originalIcon; + } +} diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java new file mode 100644 index 0000000..5187181 --- /dev/null +++ b/core/java/android/content/pm/LauncherApps.java @@ -0,0 +1,286 @@ +/* + * Copyright (C) 2014 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.content.pm; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ILauncherApps; +import android.content.pm.IOnAppsChangedListener; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Class for retrieving a list of launchable activities for the current user and any associated + * managed profiles. This is mainly for use by launchers. Apps can be queried for each user profile. + * Since the PackageManager will not deliver package broadcasts for other profiles, you can register + * for package changes here. + */ +public class LauncherApps { + + static final String TAG = "LauncherApps"; + static final boolean DEBUG = false; + + private Context mContext; + private ILauncherApps mService; + + private List<OnAppsChangedListener> mListeners + = new ArrayList<OnAppsChangedListener>(); + + /** + * Callbacks for changes to this and related managed profiles. + */ + public interface OnAppsChangedListener { + /** + * Indicates that a package was removed from the specified profile. + * + * @param user The UserHandle of the profile that generated the change. + * @param packageName The name of the package that was removed. + */ + void onPackageRemoved(UserHandle user, String packageName); + + /** + * Indicates that a package was added to the specified profile. + * + * @param user The UserHandle of the profile that generated the change. + * @param packageName The name of the package that was added. + */ + void onPackageAdded(UserHandle user, String packageName); + + /** + * Indicates that a package was modified in the specified profile. + * + * @param user The UserHandle of the profile that generated the change. + * @param packageName The name of the package that has changed. + */ + void onPackageChanged(UserHandle user, String packageName); + + /** + * Indicates that one or more packages have become available. For + * example, this can happen when a removable storage card has + * reappeared. + * + * @param user The UserHandle of the profile that generated the change. + * @param packageNames The names of the packages that have become + * available. + * @param replacing Indicates whether these packages are replacing + * existing ones. + */ + void onPackagesAvailable(UserHandle user, String[] packageNames, boolean replacing); + + /** + * Indicates that one or more packages have become unavailable. For + * example, this can happen when a removable storage card has been + * removed. + * + * @param user The UserHandle of the profile that generated the change. + * @param packageNames The names of the packages that have become + * unavailable. + * @param replacing Indicates whether the packages are about to be + * replaced with new versions. + */ + void onPackagesUnavailable(UserHandle user, String[] packageNames, boolean replacing); + } + + /** @hide */ + public LauncherApps(Context context, ILauncherApps service) { + mContext = context; + mService = service; + } + + /** + * Retrieves a list of launchable activities that match {@link Intent#ACTION_MAIN} and + * {@link Intent#CATEGORY_LAUNCHER}, for a specified user. + * + * @param packageName The specific package to query. If null, it checks all installed packages + * in the profile. + * @param user The UserHandle of the profile. + * @return List of launchable activities. Can be an empty list but will not be null. + */ + public List<LauncherActivityInfo> getActivityList(String packageName, UserHandle user) { + List<ResolveInfo> activities = null; + try { + activities = mService.getLauncherActivities(packageName, user); + } catch (RemoteException re) { + } + if (activities == null) { + return Collections.EMPTY_LIST; + } + ArrayList<LauncherActivityInfo> lais = new ArrayList<LauncherActivityInfo>(); + final int count = activities.size(); + for (int i = 0; i < count; i++) { + ResolveInfo ri = activities.get(i); + LauncherActivityInfo lai = new LauncherActivityInfo(mContext, ri, user); + if (DEBUG) { + Log.v(TAG, "Returning activity for profile " + user + " : " + + lai.getComponentName()); + } + lais.add(lai); + } + return lais; + } + + static ComponentName getComponentName(ResolveInfo ri) { + return new ComponentName(ri.activityInfo.packageName, ri.activityInfo.name); + } + + /** + * Returns the activity info for a given intent and user handle, if it resolves. Otherwise it + * returns null. + * + * @param intent The intent to find a match for. + * @param user The profile to look in for a match. + * @return An activity info object if there is a match. + */ + public LauncherActivityInfo resolveActivity(Intent intent, UserHandle user) { + try { + ResolveInfo ri = mService.resolveActivity(intent, user); + if (ri != null) { + LauncherActivityInfo info = new LauncherActivityInfo(mContext, ri, user); + return info; + } + } catch (RemoteException re) { + return null; + } + return null; + } + + /** + * Starts an activity in the specified profile. + * + * @param component The ComponentName of the activity to launch + * @param sourceBounds The Rect containing the source bounds of the clicked icon + * @param opts Options to pass to startActivity + * @param user The UserHandle of the profile + */ + public void startActivityForProfile(ComponentName component, Rect sourceBounds, + Bundle opts, UserHandle user) { + if (DEBUG) { + Log.i(TAG, "StartActivityForProfile " + component + " " + user.getIdentifier()); + } + try { + mService.startActivityAsUser(component, sourceBounds, opts, user); + } catch (RemoteException re) { + // Oops! + } + } + + /** + * Adds a listener for changes to packages in current and managed profiles. + * + * @param listener The listener to add. + */ + public synchronized void addOnAppsChangedListener(OnAppsChangedListener listener) { + if (listener != null && !mListeners.contains(listener)) { + mListeners.add(listener); + if (mListeners.size() == 1) { + try { + mService.addOnAppsChangedListener(mAppsChangedListener); + } catch (RemoteException re) { + } + } + } + } + + /** + * Removes a listener that was previously added. + * + * @param listener The listener to remove. + * @see #addOnAppsChangedListener(OnAppsChangedListener) + */ + public synchronized void removeOnAppsChangedListener(OnAppsChangedListener listener) { + mListeners.remove(listener); + if (mListeners.size() == 0) { + try { + mService.removeOnAppsChangedListener(mAppsChangedListener); + } catch (RemoteException re) { + } + } + } + + private IOnAppsChangedListener.Stub mAppsChangedListener = new IOnAppsChangedListener.Stub() { + + @Override + public void onPackageRemoved(UserHandle user, String packageName) throws RemoteException { + if (DEBUG) { + Log.d(TAG, "onPackageRemoved " + user.getIdentifier() + "," + packageName); + } + synchronized (LauncherApps.this) { + for (OnAppsChangedListener listener : mListeners) { + listener.onPackageRemoved(user, packageName); + } + } + } + + @Override + public void onPackageChanged(UserHandle user, String packageName) throws RemoteException { + if (DEBUG) { + Log.d(TAG, "onPackageChanged " + user.getIdentifier() + "," + packageName); + } + synchronized (LauncherApps.this) { + for (OnAppsChangedListener listener : mListeners) { + listener.onPackageChanged(user, packageName); + } + } + } + + @Override + public void onPackageAdded(UserHandle user, String packageName) throws RemoteException { + if (DEBUG) { + Log.d(TAG, "onPackageAdded " + user.getIdentifier() + "," + packageName); + } + synchronized (LauncherApps.this) { + for (OnAppsChangedListener listener : mListeners) { + listener.onPackageAdded(user, packageName); + } + } + } + + @Override + public void onPackagesAvailable(UserHandle user, String[] packageNames, boolean replacing) + throws RemoteException { + if (DEBUG) { + Log.d(TAG, "onPackagesAvailable " + user.getIdentifier() + "," + packageNames); + } + synchronized (LauncherApps.this) { + for (OnAppsChangedListener listener : mListeners) { + listener.onPackagesAvailable(user, packageNames, replacing); + } + } + } + + @Override + public void onPackagesUnavailable(UserHandle user, String[] packageNames, boolean replacing) + throws RemoteException { + if (DEBUG) { + Log.d(TAG, "onPackagesUnavailable " + user.getIdentifier() + "," + packageNames); + } + synchronized (LauncherApps.this) { + for (OnAppsChangedListener listener : mListeners) { + listener.onPackagesUnavailable(user, packageNames, replacing); + } + } + } + }; +} diff --git a/core/java/android/content/pm/PackageInfo.java b/core/java/android/content/pm/PackageInfo.java index 785f2b4..ef0c4d5 100644 --- a/core/java/android/content/pm/PackageInfo.java +++ b/core/java/android/content/pm/PackageInfo.java @@ -209,6 +209,19 @@ public class PackageInfo implements Parcelable { */ public static final int INSTALL_LOCATION_PREFER_EXTERNAL = 2; /** + * Flag for {@link #requiredForProfile} + * The application will always be installed for a restricted profile. + * @hide + */ + public static final int RESTRICTED_PROFILE = 1; + /** + * Flag for {@link #requiredForProfile} + * The application will always be installed for a managed profile. + * @hide + */ + public static final int MANAGED_PROFILE = 2; + + /** * The install location requested by the activity. From the * {@link android.R.attr#installLocation} attribute, one of * {@link #INSTALL_LOCATION_AUTO}, @@ -218,6 +231,12 @@ public class PackageInfo implements Parcelable { */ public int installLocation = INSTALL_LOCATION_INTERNAL_ONLY; + /** + * Defines which profiles this app is required for. + * @hide + */ + public int requiredForProfile; + /** @hide */ public boolean requiredForAllUsers; @@ -276,6 +295,7 @@ public class PackageInfo implements Parcelable { dest.writeTypedArray(reqFeatures, parcelableFlags); dest.writeInt(installLocation); dest.writeInt(requiredForAllUsers ? 1 : 0); + dest.writeInt(requiredForProfile); dest.writeString(restrictedAccountType); dest.writeString(requiredAccountType); dest.writeString(overlayTarget); @@ -318,6 +338,7 @@ public class PackageInfo implements Parcelable { reqFeatures = source.createTypedArray(FeatureInfo.CREATOR); installLocation = source.readInt(); requiredForAllUsers = source.readInt() != 0; + requiredForProfile = source.readInt(); restrictedAccountType = source.readString(); requiredAccountType = source.readString(); overlayTarget = source.readString(); diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 226f5a6..d981cc1 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -16,8 +16,10 @@ package android.content.pm; +import android.annotation.IntDef; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; +import android.app.PackageInstallObserver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -33,6 +35,8 @@ import android.util.AndroidException; import android.util.DisplayMetrics; import java.io.File; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.List; /** @@ -190,6 +194,11 @@ public abstract class PackageManager { */ public static final int MATCH_DEFAULT_ONLY = 0x00010000; + /** @hide */ + @IntDef({PERMISSION_GRANTED, PERMISSION_DENIED}) + @Retention(RetentionPolicy.SOURCE) + public @interface PermissionResult {} + /** * Permission check result: this is returned by {@link #checkPermission} * if the permission has been granted to the given package. @@ -677,12 +686,26 @@ public abstract class PackageManager { /** * Installation failed return code: this is passed to the {@link IPackageInstallObserver} by * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} + * if the system failed to install the package because it is attempting to define a + * permission that is already defined by some existing package. + * + * <p>The package name of the app which has already defined the permission is passed to + * a {@link IPackageInstallObserver2}, if any, as the {@link #EXTRA_EXISTING_PACKAGE} + * string extra; and the name of the permission being redefined is passed in the + * {@link #EXTRA_EXISTING_PERMISSION} string extra. + * @hide + */ + public static final int INSTALL_FAILED_DUPLICATE_PERMISSION = -112; + + /** + * Installation failed return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} * if the system failed to install the package because its packaged native code did not * match any of the ABIs supported by the system. * * @hide */ - public static final int INSTALL_FAILED_NO_MATCHING_ABIS = -112; + public static final int INSTALL_FAILED_NO_MATCHING_ABIS = -113; /** * Internal return code for NativeLibraryHelper methods to indicate that the package @@ -691,7 +714,7 @@ public abstract class PackageManager { * * @hide */ - public static final int NO_NATIVE_LIBRARIES = -113; + public static final int NO_NATIVE_LIBRARIES = -114; /** * Flag parameter for {@link #deletePackage} to indicate that you don't want to delete the @@ -1314,6 +1337,13 @@ public abstract class PackageManager { public static final String FEATURE_BACKUP = "android.software.backup"; /** + * Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}: + * The device supports managed profiles for enterprise users. + */ + @SdkConstant(SdkConstantType.FEATURE) + public static final String FEATURE_MANAGEDPROFILES = "android.software.managedprofiles"; + + /** * Action to external storage service to clean out removed apps. * @hide */ @@ -1403,6 +1433,24 @@ public abstract class PackageManager { = "android.content.pm.extra.PERMISSION_LIST"; /** + * String extra for {@link IPackageInstallObserver2} in the 'extras' Bundle in case of + * {@link #INSTALL_FAILED_DUPLICATE_PERMISSION}. This extra names the package which provides + * the existing definition for the permission. + * @hide + */ + public static final String EXTRA_FAILURE_EXISTING_PACKAGE + = "android.content.pm.extra.FAILURE_EXISTING_PACKAGE"; + + /** + * String extra for {@link IPackageInstallObserver2} in the 'extras' Bundle in case of + * {@link #INSTALL_FAILED_DUPLICATE_PERMISSION}. This extra names the permission that is + * being redundantly defined by the package being installed. + * @hide + */ + public static final String EXTRA_FAILURE_EXISTING_PERMISSION + = "android.content.pm.extra.FAILURE_EXISTING_PERMISSION"; + + /** * Retrieve overall information about an application package that is * installed on the system. * <p> @@ -2781,11 +2829,14 @@ public abstract class PackageManager { * 'content:' URI. * @param observer An observer callback to get notified when the package installation is * complete. {@link IPackageInstallObserver#packageInstalled(String, int)} will be - * called when that happens. observer may be null to indicate that no callback is desired. + * called when that happens. This parameter must not be null. * @param flags - possible values: {@link #INSTALL_FORWARD_LOCK}, * {@link #INSTALL_REPLACE_EXISTING}, {@link #INSTALL_ALLOW_TEST}. * @param installerPackageName Optional package name of the application that is performing the * installation. This identifies which market the package came from. + * @deprecated Use {@link #installPackage(Uri, IPackageInstallObserver2, int, String)} + * instead. This method will continue to be supported but the older observer interface + * will not get additional failure details. */ public abstract void installPackage( Uri packageURI, IPackageInstallObserver observer, int flags, @@ -2801,11 +2852,9 @@ public abstract class PackageManager { * @param observer An observer callback to get notified when the package * installation is complete. * {@link IPackageInstallObserver#packageInstalled(String, int)} - * will be called when that happens. observer may be null to - * indicate that no callback is desired. + * will be called when that happens. This parameter must not be null. * @param flags - possible values: {@link #INSTALL_FORWARD_LOCK}, - * {@link #INSTALL_REPLACE_EXISTING}, {@link #INSTALL_ALLOW_TEST} - * . + * {@link #INSTALL_REPLACE_EXISTING}, {@link #INSTALL_ALLOW_TEST}. * @param installerPackageName Optional package name of the application that * is performing the installation. This identifies which market * the package came from. @@ -2818,6 +2867,10 @@ public abstract class PackageManager { * these parameters describing the encryption and authentication * used. May be {@code null}. * @hide + * @deprecated Use {@link #installPackageWithVerification(Uri, IPackageInstallObserver2, + * int, String, Uri, ManifestDigest, ContainerEncryptionParams)} instead. This method will + * continue to be supported but the older observer interface will not get additional failure + * details. */ public abstract void installPackageWithVerification(Uri packageURI, IPackageInstallObserver observer, int flags, String installerPackageName, @@ -2834,11 +2887,9 @@ public abstract class PackageManager { * @param observer An observer callback to get notified when the package * installation is complete. * {@link IPackageInstallObserver#packageInstalled(String, int)} - * will be called when that happens. observer may be null to - * indicate that no callback is desired. + * will be called when that happens. This parameter must not be null. * @param flags - possible values: {@link #INSTALL_FORWARD_LOCK}, - * {@link #INSTALL_REPLACE_EXISTING}, {@link #INSTALL_ALLOW_TEST} - * . + * {@link #INSTALL_REPLACE_EXISTING}, {@link #INSTALL_ALLOW_TEST}. * @param installerPackageName Optional package name of the application that * is performing the installation. This identifies which market * the package came from. @@ -2849,12 +2900,101 @@ public abstract class PackageManager { * used. May be {@code null}. * * @hide + * @deprecated Use {@link #installPackageWithVerificationAndEncryption(Uri, + * IPackageInstallObserver2, int, String, VerificationParams, + * ContainerEncryptionParams)} instead. This method will continue to be + * supported but the older observer interface will not get additional failure details. */ + @Deprecated public abstract void installPackageWithVerificationAndEncryption(Uri packageURI, IPackageInstallObserver observer, int flags, String installerPackageName, VerificationParams verificationParams, ContainerEncryptionParams encryptionParams); + // Package-install variants that take the new, expanded form of observer interface. + // Note that these *also* take the original observer type and will redundantly + // report the same information to that observer if supplied; but it is not required. + + /** + * @hide + * + * Install a package. Since this may take a little while, the result will + * be posted back to the given observer. An installation will fail if the calling context + * lacks the {@link android.Manifest.permission#INSTALL_PACKAGES} permission, if the + * package named in the package file's manifest is already installed, or if there's no space + * available on the device. + * + * @param packageURI The location of the package file to install. This can be a 'file:' or a + * 'content:' URI. + * @param observer An observer callback to get notified when the package installation is + * complete. {@link PackageInstallObserver#packageInstalled(String, Bundle, int)} will be + * called when that happens. This parameter must not be null. + * @param flags - possible values: {@link #INSTALL_FORWARD_LOCK}, + * {@link #INSTALL_REPLACE_EXISTING}, {@link #INSTALL_ALLOW_TEST}. + * @param installerPackageName Optional package name of the application that is performing the + * installation. This identifies which market the package came from. + */ + public abstract void installPackage( + Uri packageURI, PackageInstallObserver observer, + int flags, String installerPackageName); + + /** + * Similar to + * {@link #installPackage(Uri, IPackageInstallObserver, int, String)} but + * with an extra verification file provided. + * + * @param packageURI The location of the package file to install. This can + * be a 'file:' or a 'content:' URI. + * @param observer An observer callback to get notified when the package installation is + * complete. {@link PackageInstallObserver#packageInstalled(String, Bundle, int)} will be + * called when that happens. This parameter must not be null. + * @param flags - possible values: {@link #INSTALL_FORWARD_LOCK}, + * {@link #INSTALL_REPLACE_EXISTING}, {@link #INSTALL_ALLOW_TEST}. + * @param installerPackageName Optional package name of the application that + * is performing the installation. This identifies which market + * the package came from. + * @param verificationURI The location of the supplementary verification + * file. This can be a 'file:' or a 'content:' URI. May be + * {@code null}. + * @param manifestDigest an object that holds the digest of the package + * which can be used to verify ownership. May be {@code null}. + * @param encryptionParams if the package to be installed is encrypted, + * these parameters describing the encryption and authentication + * used. May be {@code null}. + * @hide + */ + public abstract void installPackageWithVerification(Uri packageURI, + PackageInstallObserver observer, int flags, String installerPackageName, + Uri verificationURI, ManifestDigest manifestDigest, + ContainerEncryptionParams encryptionParams); + + /** + * Similar to + * {@link #installPackage(Uri, IPackageInstallObserver, int, String)} but + * with an extra verification information provided. + * + * @param packageURI The location of the package file to install. This can + * be a 'file:' or a 'content:' URI. + * @param observer An observer callback to get notified when the package installation is + * complete. {@link PackageInstallObserver#packageInstalled(String, Bundle, int)} will be + * called when that happens. This parameter must not be null. + * @param flags - possible values: {@link #INSTALL_FORWARD_LOCK}, + * {@link #INSTALL_REPLACE_EXISTING}, {@link #INSTALL_ALLOW_TEST}. + * @param installerPackageName Optional package name of the application that + * is performing the installation. This identifies which market + * the package came from. + * @param verificationParams an object that holds signal information to + * assist verification. May be {@code null}. + * @param encryptionParams if the package to be installed is encrypted, + * these parameters describing the encryption and authentication + * used. May be {@code null}. + * + * @hide + */ + public abstract void installPackageWithVerificationAndEncryption(Uri packageURI, + PackageInstallObserver observer, int flags, String installerPackageName, + VerificationParams verificationParams, ContainerEncryptionParams encryptionParams); + /** * If there is already an application with the given package name installed * on the system for other users, also install it for the calling user. diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index b3b6e09..8d8d220 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -57,7 +57,6 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.jar.JarEntry; import java.util.jar.StrictJarFile; import java.util.zip.ZipEntry; @@ -306,6 +305,7 @@ public class PackageParser { if ((pi.applicationInfo.flags&ApplicationInfo.FLAG_SYSTEM) != 0 || (pi.applicationInfo.flags&ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0) { pi.requiredForAllUsers = p.mRequiredForAllUsers; + pi.requiredForProfile = p.mRequiredForProfile; } pi.restrictedAccountType = p.mRestrictedAccountType; pi.requiredAccountType = p.mRequiredAccountType; @@ -988,7 +988,7 @@ public class PackageParser { TypedArray sa = res.obtainAttributes(attrs, com.android.internal.R.styleable.AndroidManifest); - pkg.mVersionCode = sa.getInteger( + pkg.mVersionCode = pkg.applicationInfo.versionCode = sa.getInteger( com.android.internal.R.styleable.AndroidManifest_versionCode, 0); pkg.mVersionName = sa.getNonConfigurationString( com.android.internal.R.styleable.AndroidManifest_versionName, 0); @@ -1009,13 +1009,14 @@ public class PackageParser { pkg.mSharedUserLabel = sa.getResourceId( com.android.internal.R.styleable.AndroidManifest_sharedUserLabel, 0); } - sa.recycle(); pkg.installLocation = sa.getInteger( com.android.internal.R.styleable.AndroidManifest_installLocation, PARSE_DEFAULT_INSTALL_LOCATION); pkg.applicationInfo.installLocation = pkg.installLocation; + sa.recycle(); + /* Set the global "forward lock" flag */ if ((flags & PARSE_FORWARD_LOCK) != 0) { pkg.applicationInfo.flags |= ApplicationInfo.FLAG_FORWARD_LOCK; @@ -1104,7 +1105,6 @@ public class PackageParser { if (!parseUsesPermission(pkg, res, parser, attrs, outError)) { return null; } - } else if (tagName.equals("uses-configuration")) { ConfigurationInfo cPref = new ConfigurationInfo(); sa = res.obtainAttributes(attrs, @@ -1986,6 +1986,8 @@ public class PackageParser { false)) { owner.mRequiredForAllUsers = true; } + owner.mRequiredForProfile = sa.getInt( + com.android.internal.R.styleable.AndroidManifestApplication_requiredForProfile, 0); String restrictedAccountType = sa.getString(com.android.internal.R.styleable .AndroidManifestApplication_restrictedAccountType); @@ -2445,6 +2447,11 @@ public class PackageParser { a.info.flags |= ActivityInfo.FLAG_IMMERSIVE; } + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestActivity_persistable, false)) { + a.info.flags |= ActivityInfo.FLAG_PERSISTABLE; + } + if (!receiver) { if (sa.getBoolean( com.android.internal.R.styleable.AndroidManifestActivity_hardwareAccelerated, @@ -3285,19 +3292,23 @@ public class PackageParser { if (packageName == null || packageName.length() == 0) { Slog.i(TAG, "verifier package name was null; skipping"); return null; - } else if (encodedPublicKey == null) { - Slog.i(TAG, "verifier " + packageName + " public key was null; skipping"); } - PublicKey publicKey = parsePublicKey(encodedPublicKey); - if (publicKey != null) { - return new VerifierInfo(packageName, publicKey); + final PublicKey publicKey = parsePublicKey(encodedPublicKey); + if (publicKey == null) { + Slog.i(TAG, "Unable to parse verifier public key for " + packageName); + return null; } - return null; + return new VerifierInfo(packageName, publicKey); } - public static final PublicKey parsePublicKey(String encodedPublicKey) { + public static final PublicKey parsePublicKey(final String encodedPublicKey) { + if (encodedPublicKey == null) { + Slog.i(TAG, "Could not parse null public key"); + return null; + } + EncodedKeySpec keySpec; try { final byte[] encoded = Base64.decode(encodedPublicKey, Base64.DEFAULT); @@ -3591,6 +3602,9 @@ public class PackageParser { /* An app that's required for all users and cannot be uninstalled for a user */ public boolean mRequiredForAllUsers; + /* For which types of profile this app is required */ + public int mRequiredForProfile; + /* The restricted account authenticator type that is used by this application */ public String mRestrictedAccountType; diff --git a/core/java/android/content/pm/RegisteredServicesCache.java b/core/java/android/content/pm/RegisteredServicesCache.java index 875e8de..4a743a5 100644 --- a/core/java/android/content/pm/RegisteredServicesCache.java +++ b/core/java/android/content/pm/RegisteredServicesCache.java @@ -140,12 +140,33 @@ public abstract class RegisteredServicesCache<V> { mContext.registerReceiver(mExternalReceiver, sdFilter); } + private final void handlePackageEvent(Intent intent, int userId) { + // Don't regenerate the services map when the package is removed or its + // ASEC container unmounted as a step in replacement. The subsequent + // _ADDED / _AVAILABLE call will regenerate the map in the final state. + final String action = intent.getAction(); + // it's a new-component action if it isn't some sort of removal + final boolean isRemoval = Intent.ACTION_PACKAGE_REMOVED.equals(action) + || Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action); + // if it's a removal, is it part of an update-in-place step? + final boolean replacing = intent.getBooleanExtra(Intent.EXTRA_REPLACING, false); + + if (isRemoval && replacing) { + // package is going away, but it's the middle of an upgrade: keep the current + // state and do nothing here. This clause is intentionally empty. + } else { + // either we're adding/changing, or it's a removal without replacement, so + // we need to recalculate the set of available services + generateServicesMap(userId); + } + } + private final BroadcastReceiver mPackageReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1); if (uid != -1) { - generateServicesMap(UserHandle.getUserId(uid)); + handlePackageEvent(intent, UserHandle.getUserId(uid)); } } }; @@ -154,7 +175,7 @@ public abstract class RegisteredServicesCache<V> { @Override public void onReceive(Context context, Intent intent) { // External apps can't coexist with multi-user, so scan owner - generateServicesMap(UserHandle.USER_OWNER); + handlePackageEvent(intent, UserHandle.USER_OWNER); } }; diff --git a/core/java/android/content/pm/UserInfo.java b/core/java/android/content/pm/UserInfo.java index 4c87830..f53aa4c 100644 --- a/core/java/android/content/pm/UserInfo.java +++ b/core/java/android/content/pm/UserInfo.java @@ -18,6 +18,7 @@ package android.content.pm; import android.os.Parcel; import android.os.Parcelable; +import android.os.SystemProperties; import android.os.UserHandle; /** @@ -63,6 +64,15 @@ public class UserInfo implements Parcelable { */ public static final int FLAG_INITIALIZED = 0x00000010; + /** + * Indicates that this user is a profile of another user, for example holding a users + * corporate data. + */ + public static final int FLAG_MANAGED_PROFILE = 0x00000020; + + + public static final int NO_PROFILE_GROUP_ID = -1; + public int id; public int serialNumber; public String name; @@ -70,6 +80,7 @@ public class UserInfo implements Parcelable { public int flags; public long creationTime; public long lastLoggedInTime; + public int profileGroupId; /** User is only partially created. */ public boolean partial; @@ -83,6 +94,7 @@ public class UserInfo implements Parcelable { this.name = name; this.flags = flags; this.iconPath = iconPath; + this.profileGroupId = NO_PROFILE_GROUP_ID; } public boolean isPrimary() { @@ -101,6 +113,18 @@ public class UserInfo implements Parcelable { return (flags & FLAG_RESTRICTED) == FLAG_RESTRICTED; } + public boolean isManagedProfile() { + return (flags & FLAG_MANAGED_PROFILE) == FLAG_MANAGED_PROFILE; + } + + /** + * @return true if this user can be switched to. + **/ + public boolean supportsSwitchTo() { + // TODO remove fw.show_hidden_users when we have finished developing managed profiles. + return !isManagedProfile() || SystemProperties.getBoolean("fw.show_hidden_users", false); + } + public UserInfo() { } @@ -113,6 +137,7 @@ public class UserInfo implements Parcelable { creationTime = orig.creationTime; lastLoggedInTime = orig.lastLoggedInTime; partial = orig.partial; + profileGroupId = orig.profileGroupId; } public UserHandle getUserHandle() { @@ -137,6 +162,7 @@ public class UserInfo implements Parcelable { dest.writeLong(creationTime); dest.writeLong(lastLoggedInTime); dest.writeInt(partial ? 1 : 0); + dest.writeInt(profileGroupId); } public static final Parcelable.Creator<UserInfo> CREATOR @@ -158,5 +184,6 @@ public class UserInfo implements Parcelable { creationTime = source.readLong(); lastLoggedInTime = source.readLong(); partial = source.readInt() != 0; + profileGroupId = source.readInt(); } } diff --git a/core/java/android/content/pm/XmlSerializerAndParser.java b/core/java/android/content/pm/XmlSerializerAndParser.java index 935fc02..20cb61c 100644 --- a/core/java/android/content/pm/XmlSerializerAndParser.java +++ b/core/java/android/content/pm/XmlSerializerAndParser.java @@ -19,7 +19,6 @@ package android.content.pm; import org.xmlpull.v1.XmlSerializer; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; -import android.os.Parcel; import java.io.IOException; diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java index 9ce17e4..0c04401 100644 --- a/core/java/android/content/res/AssetManager.java +++ b/core/java/android/content/res/AssetManager.java @@ -17,8 +17,8 @@ package android.content.res; import android.os.ParcelFileDescriptor; -import android.os.Trace; import android.util.Log; +import android.util.SparseArray; import android.util.TypedValue; import java.io.FileNotFoundException; @@ -267,11 +267,9 @@ public final class AssetManager { } } - /*package*/ final CharSequence getPooledString(int block, int id) { - //System.out.println("Get pooled: block=" + block - // + ", id=#" + Integer.toHexString(id) - // + ", blocks=" + mStringBlocks); - return mStringBlocks[block-1].get(id); + /*package*/ final CharSequence getPooledStringForCookie(int cookie, int id) { + // Cookies map to string blocks starting at 1. + return mStringBlocks[cookie - 1].get(id); } /** @@ -536,6 +534,9 @@ public final class AssetManager { } public final class AssetInputStream extends InputStream { + /** + * @hide + */ public final int getAssetInt() { throw new UnsupportedOperationException(); } @@ -720,6 +721,9 @@ public final class AssetManager { /*package*/ native static final boolean applyStyle(long theme, int defStyleAttr, int defStyleRes, long xmlParser, int[] inAttrs, int[] outValues, int[] outIndices); + /*package*/ native static final boolean resolveAttrs(long theme, + int defStyleAttr, int defStyleRes, int[] inValues, + int[] inAttrs, int[] outValues, int[] outIndices); /*package*/ native final boolean retrieveAttributes( long xmlParser, int[] inAttrs, int[] outValues, int[] outIndices); /*package*/ native final int getArraySize(int resource); @@ -735,6 +739,11 @@ public final class AssetManager { /** * {@hide} */ + public native final SparseArray<String> getAssignedPackageIdentifiers(); + + /** + * {@hide} + */ public native static final int getGlobalAssetCount(); /** diff --git a/core/java/android/content/res/ColorStateList.java b/core/java/android/content/res/ColorStateList.java index bd23db4..5674154 100644 --- a/core/java/android/content/res/ColorStateList.java +++ b/core/java/android/content/res/ColorStateList.java @@ -16,12 +16,16 @@ package android.content.res; +import android.graphics.Color; + import com.android.internal.util.ArrayUtils; +import com.android.internal.util.GrowingArrayUtils; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import android.util.AttributeSet; +import android.util.MathUtils; import android.util.SparseArray; import android.util.StateSet; import android.util.Xml; @@ -171,17 +175,16 @@ public class ColorStateList implements Parcelable { * Fill in this object based on the contents of an XML "selector" element. */ private void inflate(Resources r, XmlPullParser parser, AttributeSet attrs) - throws XmlPullParserException, IOException { + throws XmlPullParserException, IOException { int type; final int innerDepth = parser.getDepth()+1; int depth; - int listAllocated = 20; + int[][] stateSpecList = ArrayUtils.newUnpaddedArray(int[].class, 20); + int[] colorList = new int[stateSpecList.length]; int listSize = 0; - int[] colorList = new int[listAllocated]; - int[][] stateSpecList = new int[listAllocated][]; while ((type=parser.next()) != XmlPullParser.END_DOCUMENT && ((depth=parser.getDepth()) >= innerDepth @@ -194,6 +197,8 @@ public class ColorStateList implements Parcelable { continue; } + int alphaRes = 0; + float alpha = 1.0f; int colorRes = 0; int color = 0xffff0000; boolean haveColor = false; @@ -205,17 +210,20 @@ public class ColorStateList implements Parcelable { for (i = 0; i < numAttrs; i++) { final int stateResId = attrs.getAttributeNameResource(i); if (stateResId == 0) break; - if (stateResId == com.android.internal.R.attr.color) { + if (stateResId == com.android.internal.R.attr.alpha) { + alphaRes = attrs.getAttributeResourceValue(i, 0); + if (alphaRes == 0) { + alpha = attrs.getAttributeFloatValue(i, 1.0f); + } + } else if (stateResId == com.android.internal.R.attr.color) { colorRes = attrs.getAttributeResourceValue(i, 0); - if (colorRes == 0) { color = attrs.getAttributeIntValue(i, color); haveColor = true; } } else { stateSpec[j++] = attrs.getAttributeBooleanValue(i, false) - ? stateResId - : -stateResId; + ? stateResId : -stateResId; } } stateSpec = StateSet.trimStateSet(stateSpec, j); @@ -228,25 +236,20 @@ public class ColorStateList implements Parcelable { + ": <item> tag requires a 'android:color' attribute."); } - if (listSize == 0 || stateSpec.length == 0) { - mDefaultColor = color; + if (alphaRes != 0) { + alpha = r.getFraction(alphaRes, 1, 1); } - - if (listSize + 1 >= listAllocated) { - listAllocated = ArrayUtils.idealIntArraySize(listSize + 1); - int[] ncolor = new int[listAllocated]; - System.arraycopy(colorList, 0, ncolor, 0, listSize); + // Apply alpha modulation. + final int alphaMod = MathUtils.constrain((int) (Color.alpha(color) * alpha), 0, 255); + color = (color & 0xFFFFFF) | (alphaMod << 24); - int[][] nstate = new int[listAllocated][]; - System.arraycopy(stateSpecList, 0, nstate, 0, listSize); - - colorList = ncolor; - stateSpecList = nstate; + if (listSize == 0 || stateSpec.length == 0) { + mDefaultColor = color; } - colorList[listSize] = color; - stateSpecList[listSize] = stateSpec; + colorList = GrowingArrayUtils.append(colorList, listSize, color); + stateSpecList = GrowingArrayUtils.append(stateSpecList, listSize, stateSpec); listSize++; } @@ -259,7 +262,17 @@ public class ColorStateList implements Parcelable { public boolean isStateful() { return mStateSpecs.length > 1; } - + + public boolean isOpaque() { + final int n = mColors.length; + for (int i = 0; i < n; i++) { + if (Color.alpha(mColors[i]) != 0xFF) { + return false; + } + } + return true; + } + /** * Return the color associated with the given set of {@link android.view.View} states. * @@ -289,6 +302,25 @@ public class ColorStateList implements Parcelable { return mDefaultColor; } + /** + * Return the states in this {@link ColorStateList}. + * @return the states in this {@link ColorStateList} + * @hide + */ + public int[][] getStates() { + return mStateSpecs; + } + + /** + * Return the colors in this {@link ColorStateList}. + * @return the colors in this {@link ColorStateList} + * @hide + */ + public int[] getColors() { + return mColors; + } + + @Override public String toString() { return "ColorStateList{" + "mStateSpecs=" + Arrays.deepToString(mStateSpecs) + @@ -296,14 +328,16 @@ public class ColorStateList implements Parcelable { "mDefaultColor=" + mDefaultColor + '}'; } + @Override public int describeContents() { return 0; } + @Override public void writeToParcel(Parcel dest, int flags) { final int N = mStateSpecs.length; dest.writeInt(N); - for (int i=0; i<N; i++) { + for (int i = 0; i < N; i++) { dest.writeIntArray(mStateSpecs[i]); } dest.writeIntArray(mColors); @@ -311,17 +345,19 @@ public class ColorStateList implements Parcelable { public static final Parcelable.Creator<ColorStateList> CREATOR = new Parcelable.Creator<ColorStateList>() { + @Override public ColorStateList[] newArray(int size) { return new ColorStateList[size]; } + @Override public ColorStateList createFromParcel(Parcel source) { final int N = source.readInt(); - int[][] stateSpecs = new int[N][]; - for (int i=0; i<N; i++) { + final int[][] stateSpecs = new int[N][]; + for (int i = 0; i < N; i++) { stateSpecs[i] = source.createIntArray(); } - int[] colors = source.createIntArray(); + final int[] colors = source.createIntArray(); return new ColorStateList(stateSpecs, colors); } }; diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java index 3889ce0..4879c23 100644 --- a/core/java/android/content/res/Resources.java +++ b/core/java/android/content/res/Resources.java @@ -34,9 +34,9 @@ import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; import android.util.Slog; +import android.util.SparseArray; import android.util.TypedValue; import android.util.LongSparseArray; -import android.view.DisplayAdjustments; import java.io.IOException; import java.io.InputStream; @@ -72,74 +72,94 @@ import libcore.icu.NativePluralRules; */ public class Resources { static final String TAG = "Resources"; + private static final boolean DEBUG_LOAD = false; private static final boolean DEBUG_CONFIG = false; - private static final boolean DEBUG_ATTRIBUTES_CACHE = false; private static final boolean TRACE_FOR_PRELOAD = false; private static final boolean TRACE_FOR_MISS_PRELOAD = false; + private static final int LAYOUT_DIR_CONFIG = ActivityInfo.activityInfoConfigToNative( + ActivityInfo.CONFIG_LAYOUT_DIRECTION); + private static final int ID_OTHER = 0x01000004; private static final Object sSync = new Object(); - /*package*/ static Resources mSystem = null; // Information about preloaded resources. Note that they are not // protected by a lock, because while preloading in zygote we are all // single-threaded, and after that these are immutable. - private static final LongSparseArray<Drawable.ConstantState>[] sPreloadedDrawables; - private static final LongSparseArray<Drawable.ConstantState> sPreloadedColorDrawables - = new LongSparseArray<Drawable.ConstantState>(); + private static final LongSparseArray<ConstantState>[] sPreloadedDrawables; + private static final LongSparseArray<ConstantState> sPreloadedColorDrawables + = new LongSparseArray<ConstantState>(); private static final LongSparseArray<ColorStateList> sPreloadedColorStateLists = new LongSparseArray<ColorStateList>(); + // Used by BridgeResources in layoutlib + static Resources mSystem = null; + private static boolean sPreloaded; private static int sPreloadedDensity; // These are protected by mAccessLock. + private final Object mAccessLock = new Object(); + private final Configuration mTmpConfig = new Configuration(); + private final ThemedCaches<ConstantState> mDrawableCache = + new ThemedCaches<ConstantState>(); + private final ThemedCaches<ConstantState> mColorDrawableCache = + new ThemedCaches<ConstantState>(); + private final LongSparseArray<WeakReference<ColorStateList>> mColorStateListCache = + new LongSparseArray<WeakReference<ColorStateList>>(); - /*package*/ final Object mAccessLock = new Object(); - /*package*/ final Configuration mTmpConfig = new Configuration(); - /*package*/ TypedValue mTmpValue = new TypedValue(); - /*package*/ final LongSparseArray<WeakReference<Drawable.ConstantState> > mDrawableCache - = new LongSparseArray<WeakReference<Drawable.ConstantState> >(0); - /*package*/ final LongSparseArray<WeakReference<ColorStateList> > mColorStateListCache - = new LongSparseArray<WeakReference<ColorStateList> >(0); - /*package*/ final LongSparseArray<WeakReference<Drawable.ConstantState> > mColorDrawableCache - = new LongSparseArray<WeakReference<Drawable.ConstantState> >(0); - /*package*/ boolean mPreloading; + private TypedValue mTmpValue = new TypedValue(); + private boolean mPreloading; - /*package*/ TypedArray mCachedStyledAttributes = null; - RuntimeException mLastRetrievedAttrs = null; + private TypedArray mCachedStyledAttributes = null; private int mLastCachedXmlBlockIndex = -1; private final int[] mCachedXmlBlockIds = { 0, 0, 0, 0 }; private final XmlBlock[] mCachedXmlBlocks = new XmlBlock[4]; - /*package*/ final AssetManager mAssets; + private final AssetManager mAssets; private final Configuration mConfiguration = new Configuration(); - /*package*/ final DisplayMetrics mMetrics = new DisplayMetrics(); + private final DisplayMetrics mMetrics = new DisplayMetrics(); private NativePluralRules mPluralRule; private CompatibilityInfo mCompatibilityInfo = CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO; + + @SuppressWarnings("unused") private WeakReference<IBinder> mToken; static { sPreloadedDrawables = new LongSparseArray[2]; - sPreloadedDrawables[0] = new LongSparseArray<Drawable.ConstantState>(); - sPreloadedDrawables[1] = new LongSparseArray<Drawable.ConstantState>(); + sPreloadedDrawables[0] = new LongSparseArray<ConstantState>(); + sPreloadedDrawables[1] = new LongSparseArray<ConstantState>(); } - /** @hide */ + /** + * Returns the most appropriate default theme for the specified target SDK version. + * <ul> + * <li>Below API 11: Gingerbread + * <li>APIs 11 thru 14: Holo + * <li>APIs 14 thru XX: Device default dark + * <li>API XX and above: Device default light with dark action bar + * </ul> + * + * @param curTheme The current theme, or 0 if not specified. + * @param targetSdkVersion The target SDK version. + * @return A theme resource identifier + * @hide + */ public static int selectDefaultTheme(int curTheme, int targetSdkVersion) { return selectSystemTheme(curTheme, targetSdkVersion, com.android.internal.R.style.Theme, com.android.internal.R.style.Theme_Holo, - com.android.internal.R.style.Theme_DeviceDefault); + com.android.internal.R.style.Theme_DeviceDefault, + com.android.internal.R.style.Theme_DeviceDefault_Light_DarkActionBar); } - + /** @hide */ - public static int selectSystemTheme(int curTheme, int targetSdkVersion, - int orig, int holo, int deviceDefault) { + public static int selectSystemTheme(int curTheme, int targetSdkVersion, int orig, int holo, + int dark, int deviceDefault) { if (curTheme != 0) { return curTheme; } @@ -149,9 +169,12 @@ public class Resources { if (targetSdkVersion < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { return holo; } + if (targetSdkVersion < Build.VERSION_CODES.CUR_DEVELOPMENT) { + return dark; + } return deviceDefault; } - + /** * This exception is thrown by the resource APIs when a requested resource * can not be found. @@ -513,7 +536,7 @@ public class Resources { + Integer.toHexString(id)); } - TypedArray array = getCachedStyledAttributes(len); + TypedArray array = TypedArray.obtain(this, len); array.mLength = mAssets.retrieveArray(id, array.mData); array.mIndices[0] = 0; @@ -681,12 +704,27 @@ public class Resources { * @param id The desired resource identifier, as generated by the aapt * tool. This integer encodes the package, type, and resource * entry. The value 0 is an invalid identifier. - * - * @throws NotFoundException Throws NotFoundException if the given ID does not exist. - * * @return Drawable An object that can be used to draw this resource. + * @throws NotFoundException Throws NotFoundException if the given ID does + * not exist. */ public Drawable getDrawable(int id) throws NotFoundException { + return getDrawable(id, null); + } + + /** + * Return a drawable object associated with a particular resource ID and + * styled for the specified theme. + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * @param theme The theme used to style the drawable attributes. + * @return Drawable An object that can be used to draw this resource. + * @throws NotFoundException Throws NotFoundException if the given ID does + * not exist. + */ + public Drawable getDrawable(int id, Theme theme) throws NotFoundException { TypedValue value; synchronized (mAccessLock) { value = mTmpValue; @@ -697,7 +735,7 @@ public class Resources { } getValue(id, value, true); } - Drawable res = loadDrawable(value, id); + final Drawable res = loadDrawable(value, id, theme); synchronized (mAccessLock) { if (mTmpValue == null) { mTmpValue = value; @@ -715,17 +753,36 @@ public class Resources { * depending on the underlying resource -- for example, a solid color, PNG * image, scalable image, etc. The Drawable API hides these implementation * details. - * + * * @param id The desired resource identifier, as generated by the aapt tool. * This integer encodes the package, type, and resource entry. * The value 0 is an invalid identifier. * @param density the desired screen density indicated by the resource as * found in {@link DisplayMetrics}. + * @return Drawable An object that can be used to draw this resource. * @throws NotFoundException Throws NotFoundException if the given ID does * not exist. - * @return Drawable An object that can be used to draw this resource. + * @see #getDrawableForDensity(int, int, Theme) */ public Drawable getDrawableForDensity(int id, int density) throws NotFoundException { + return getDrawableForDensity(id, density, null); + } + + /** + * Return a drawable object associated with a particular resource ID for the + * given screen density in DPI and styled for the specified theme. + * + * @param id The desired resource identifier, as generated by the aapt tool. + * This integer encodes the package, type, and resource entry. + * The value 0 is an invalid identifier. + * @param density The desired screen density indicated by the resource as + * found in {@link DisplayMetrics}. + * @param theme The theme used to style the drawable attributes. + * @return Drawable An object that can be used to draw this resource. + * @throws NotFoundException Throws NotFoundException if the given ID does + * not exist. + */ + public Drawable getDrawableForDensity(int id, int density, Theme theme) { TypedValue value; synchronized (mAccessLock) { value = mTmpValue; @@ -752,7 +809,7 @@ public class Resources { } } - Drawable res = loadDrawable(value, id); + final Drawable res = loadDrawable(value, id, theme); synchronized (mAccessLock) { if (mTmpValue == null) { mTmpValue = value; @@ -1211,6 +1268,10 @@ public class Resources { */ public void applyStyle(int resid, boolean force) { AssetManager.applyThemeStyle(mTheme, resid, force); + + // TODO: In very rare cases, we may end up with a hybrid theme + // that can't map to a single theme ID. + mThemeResId = resid; } /** @@ -1224,6 +1285,8 @@ public class Resources { */ public void setTo(Theme other) { AssetManager.copyTheme(mTheme, other.mTheme); + + mThemeResId = other.mThemeResId; } /** @@ -1246,11 +1309,10 @@ public class Resources { * @see #obtainStyledAttributes(AttributeSet, int[], int, int) */ public TypedArray obtainStyledAttributes(int[] attrs) { - int len = attrs.length; - TypedArray array = getCachedStyledAttributes(len); - array.mRsrcs = attrs; - AssetManager.applyStyle(mTheme, 0, 0, 0, attrs, - array.mData, array.mIndices); + final int len = attrs.length; + final TypedArray array = TypedArray.obtain(Resources.this, len); + array.mTheme = this; + AssetManager.applyStyle(mTheme, 0, 0, 0, attrs, array.mData, array.mIndices); return array; } @@ -1274,14 +1336,10 @@ public class Resources { * @see #obtainStyledAttributes(int[]) * @see #obtainStyledAttributes(AttributeSet, int[], int, int) */ - public TypedArray obtainStyledAttributes(int resid, int[] attrs) - throws NotFoundException { - int len = attrs.length; - TypedArray array = getCachedStyledAttributes(len); - array.mRsrcs = attrs; - - AssetManager.applyStyle(mTheme, 0, resid, 0, attrs, - array.mData, array.mIndices); + public TypedArray obtainStyledAttributes(int resid, int[] attrs) throws NotFoundException { + final int len = attrs.length; + final TypedArray array = TypedArray.obtain(Resources.this, len); + array.mTheme = this; if (false) { int[] data = array.mData; @@ -1308,6 +1366,7 @@ public class Resources { } System.out.println(s); } + AssetManager.applyStyle(mTheme, 0, resid, 0, attrs, array.mData, array.mIndices); return array; } @@ -1361,20 +1420,18 @@ public class Resources { */ public TypedArray obtainStyledAttributes(AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes) { - int len = attrs.length; - TypedArray array = getCachedStyledAttributes(len); + final int len = attrs.length; + final TypedArray array = TypedArray.obtain(Resources.this, len); // XXX note that for now we only work with compiled XML files. // To support generic XML files we will need to manually parse // out the attributes from the XML file (applying type information // contained in the resources and such). - XmlBlock.Parser parser = (XmlBlock.Parser)set; - AssetManager.applyStyle( - mTheme, defStyleAttr, defStyleRes, - parser != null ? parser.mParseState : 0, attrs, - array.mData, array.mIndices); + final XmlBlock.Parser parser = (XmlBlock.Parser)set; + AssetManager.applyStyle(mTheme, defStyleAttr, defStyleRes, + parser != null ? parser.mParseState : 0, attrs, array.mData, array.mIndices); - array.mRsrcs = attrs; + array.mTheme = this; array.mXml = parser; if (false) { @@ -1410,6 +1467,45 @@ public class Resources { } /** + * Retrieve the values for a set of attributes in the Theme. The + * contents of the typed array are ultimately filled in by + * {@link Resources#getValue}. + * + * @param values The base set of attribute values, must be equal + * in length to {@code attrs} or {@code null}. All values + * must be of type {@link TypedValue#TYPE_ATTRIBUTE}. + * @param attrs The desired attributes to be retrieved. + * @param defStyleAttr An attribute in the current theme that contains a + * reference to a style resource that supplies + * defaults values for the TypedArray. Can be + * 0 to not look for defaults. + * @param defStyleRes A resource identifier of a style resource that + * supplies default values for the TypedArray, + * used only if defStyleAttr is 0 or can not be found + * in the theme. Can be 0 to not look for defaults. + * @return Returns a TypedArray holding an array of the attribute + * values. Be sure to call {@link TypedArray#recycle()} + * when done with it. + * @hide + */ + public TypedArray resolveAttributes(int[] values, int[] attrs, + int defStyleAttr, int defStyleRes) { + final int len = attrs.length; + if (values != null && len != values.length) { + throw new IllegalArgumentException( + "Base attribute values must be null or the same length as attrs"); + } + + final TypedArray array = TypedArray.obtain(Resources.this, len); + AssetManager.resolveAttrs(mTheme, defStyleAttr, defStyleRes, + values, attrs, array.mData, array.mIndices); + array.mTheme = this; + array.mXml = null; + + return array; + } + + /** * Retrieve the value of an attribute in the Theme. The contents of * <var>outValue</var> are ultimately filled in by * {@link Resources#getValue}. @@ -1426,8 +1522,7 @@ public class Resources { * @return boolean Returns true if the attribute was found and * <var>outValue</var> is valid, else false. */ - public boolean resolveAttribute(int resid, TypedValue outValue, - boolean resolveRefs) { + public boolean resolveAttribute(int resid, TypedValue outValue, boolean resolveRefs) { boolean got = mAssets.getThemeValue(mTheme, resid, outValue, resolveRefs); if (false) { System.out.println( @@ -1439,6 +1534,30 @@ public class Resources { } /** + * Returns the resources to which this theme belongs. + * + * @return Resources to which this theme belongs. + */ + public Resources getResources() { + return Resources.this; + } + + /** + * Return a drawable object associated with a particular resource ID + * and styled for the Theme. + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * @return Drawable An object that can be used to draw this resource. + * @throws NotFoundException Throws NotFoundException if the given ID + * does not exist. + */ + public Drawable getDrawable(int id) throws NotFoundException { + return Resources.this.getDrawable(id, this); + } + + /** * Print contents of this theme out to the log. For debugging only. * * @param priority The log priority to use. @@ -1448,7 +1567,8 @@ public class Resources { public void dump(int priority, String tag, String prefix) { AssetManager.dumpTheme(mTheme, priority, tag, prefix); } - + + @Override protected void finalize() throws Throwable { super.finalize(); mAssets.releaseTheme(mTheme); @@ -1459,13 +1579,21 @@ public class Resources { mTheme = mAssets.createTheme(); } + @SuppressWarnings("hiding") private final AssetManager mAssets; private final long mTheme; + /** Resource identifier for the theme. */ + private int mThemeResId = 0; + // Needed by layoutlib. - /*package*/ int getNativeTheme() { + /*package*/ long getNativeTheme() { return mTheme; } + + /*package*/ int getAppliedStyleResId() { + return mThemeResId; + } } /** @@ -1492,7 +1620,7 @@ public class Resources { */ public TypedArray obtainAttributes(AttributeSet set, int[] attrs) { int len = attrs.length; - TypedArray array = getCachedStyledAttributes(len); + TypedArray array = TypedArray.obtain(this, len); // XXX note that for now we only work with compiled XML files. // To support generic XML files we will need to manually parse @@ -1502,7 +1630,6 @@ public class Resources { mAssets.retrieveAttributes(parser.mParseState, attrs, array.mData, array.mIndices); - array.mRsrcs = attrs; array.mXml = parser; return array; @@ -1574,7 +1701,7 @@ public class Resources { String locale = null; if (mConfiguration.locale != null) { - locale = mConfiguration.locale.toLanguageTag(); + locale = adjustLanguageTag(localeToLanguageTag(mConfiguration.locale)); } int width, height; if (mMetrics.widthPixels >= mMetrics.heightPixels) { @@ -1607,8 +1734,8 @@ public class Resources { + " final compat is " + mCompatibilityInfo); } - clearDrawableCacheLocked(mDrawableCache, configChanges); - clearDrawableCacheLocked(mColorDrawableCache, configChanges); + clearDrawableCachesLocked(mDrawableCache, configChanges); + clearDrawableCachesLocked(mColorDrawableCache, configChanges); mColorStateListCache.clear(); @@ -1621,18 +1748,25 @@ public class Resources { } } + private void clearDrawableCachesLocked( + ThemedCaches<ConstantState> caches, int configChanges) { + final int N = caches.size(); + for (int i = 0; i < N; i++) { + clearDrawableCacheLocked(caches.valueAt(i), configChanges); + } + } + private void clearDrawableCacheLocked( - LongSparseArray<WeakReference<ConstantState>> cache, - int configChanges) { - int N = cache.size(); + LongSparseArray<WeakReference<ConstantState>> cache, int configChanges) { if (DEBUG_CONFIG) { Log.d(TAG, "Cleaning up drawables config changes: 0x" + Integer.toHexString(configChanges)); } - for (int i=0; i<N; i++) { - WeakReference<Drawable.ConstantState> ref = cache.valueAt(i); + final int N = cache.size(); + for (int i = 0; i < N; i++) { + final WeakReference<ConstantState> ref = cache.valueAt(i); if (ref != null) { - Drawable.ConstantState cs = ref.get(); + final ConstantState cs = ref.get(); if (cs != null) { if (Configuration.needNewResources( configChanges, cs.getChangingConfigurations())) { @@ -1655,6 +1789,47 @@ public class Resources { } } + // Locale.toLanguageTag() is not available in Java6. LayoutLib overrides + // this method to enable users to use Java6. + private String localeToLanguageTag(Locale locale) { + return locale.toLanguageTag(); + } + + /** + * {@code Locale.toLanguageTag} will transform the obsolete (and deprecated) + * language codes "in", "ji" and "iw" to "id", "yi" and "he" respectively. + * + * All released versions of android prior to "L" used the deprecated language + * tags, so we will need to support them for backwards compatibility. + * + * Note that this conversion needs to take place *after* the call to + * {@code toLanguageTag} because that will convert all the deprecated codes to + * the new ones, even if they're set manually. + */ + private static String adjustLanguageTag(String languageTag) { + final int separator = languageTag.indexOf('-'); + final String language; + final String remainder; + + if (separator == -1) { + language = languageTag; + remainder = ""; + } else { + language = languageTag.substring(0, separator); + remainder = languageTag.substring(separator); + } + + if ("id".equals(language)) { + return "in" + remainder; + } else if ("yi".equals(language)) { + return "ji" + remainder; + } else if ("he".equals(language)) { + return "iw" + remainder; + } else { + return languageTag; + } + } + /** * Update the system resources configuration if they have previously * been initialized. @@ -1988,7 +2163,7 @@ public class Resources { /** * @hide */ - public LongSparseArray<Drawable.ConstantState> getPreloadedDrawables() { + public LongSparseArray<ConstantState> getPreloadedDrawables() { return sPreloadedDrawables[0]; } @@ -2006,6 +2181,8 @@ public class Resources { } catch (NotFoundException e) { resName = "?"; } + // This should never happen in production, so we should log a + // warning even if we're not debugging. Log.w(TAG, "Preloaded " + name + " resource #0x" + Integer.toHexString(resourceId) + " (" + resName + ") that varies with configuration!!"); @@ -2025,169 +2202,198 @@ public class Resources { return true; } - static private final int LAYOUT_DIR_CONFIG = ActivityInfo.activityInfoConfigToNative( - ActivityInfo.CONFIG_LAYOUT_DIRECTION); - - /*package*/ Drawable loadDrawable(TypedValue value, int id) - throws NotFoundException { - + /*package*/ Drawable loadDrawable(TypedValue value, int id, Theme theme) throws NotFoundException { if (TRACE_FOR_PRELOAD) { // Log only framework resources if ((id >>> 24) == 0x1) { final String name = getResourceName(id); - if (name != null) android.util.Log.d("PreloadDrawable", name); + if (name != null) { + Log.d("PreloadDrawable", name); + } } } - boolean isColorDrawable = false; - if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT && - value.type <= TypedValue.TYPE_LAST_COLOR_INT) { + final boolean isColorDrawable; + final ThemedCaches<ConstantState> caches; + final long key; + if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT + && value.type <= TypedValue.TYPE_LAST_COLOR_INT) { isColorDrawable = true; + caches = mColorDrawableCache; + key = value.data; + } else { + isColorDrawable = false; + caches = mDrawableCache; + key = (((long) value.assetCookie) << 32) | value.data; } - final long key = isColorDrawable ? value.data : - (((long) value.assetCookie) << 32) | value.data; - Drawable dr = getCachedDrawable(isColorDrawable ? mColorDrawableCache : mDrawableCache, key); - - if (dr != null) { - return dr; + // First, check whether we have a cached version of this drawable + // that's valid for the specified theme. This may apply a theme to a + // cached drawable that has themeable attributes but was not previously + // themed. + if (!mPreloading) { + final Drawable cachedDrawable = getCachedDrawable(caches, key, theme); + if (cachedDrawable != null) { + return cachedDrawable; + } } - Drawable.ConstantState cs; + + // Next, check preloaded drawables. These are unthemed but may have + // themeable attributes. + final ConstantState cs; if (isColorDrawable) { cs = sPreloadedColorDrawables.get(key); } else { cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key); } + + final Drawable dr; if (cs != null) { - dr = cs.newDrawable(this); + dr = cs.newDrawable(this, theme); + } else if (isColorDrawable) { + dr = new ColorDrawable(value.data); } else { - if (isColorDrawable) { - dr = new ColorDrawable(value.data); - } + dr = loadDrawableForCookie(value, id, theme); + } - if (dr == null) { - if (value.string == null) { - throw new NotFoundException( - "Resource is not a Drawable (color or path): " + value); - } + // If we were able to obtain a drawable, attempt to place it in the + // appropriate cache (e.g. no theme, themed, themeable). + if (dr != null) { + dr.setChangingConfigurations(value.changingConfigurations); + cacheDrawable(value, theme, isColorDrawable, caches, key, dr); + } - String file = value.string.toString(); + return dr; + } + + private void cacheDrawable(TypedValue value, Theme theme, boolean isColorDrawable, + ThemedCaches<ConstantState> caches, long key, Drawable dr) { + final ConstantState cs = dr.getConstantState(); + if (cs == null) { + return; + } - if (TRACE_FOR_MISS_PRELOAD) { - // Log only framework resources - if ((id >>> 24) == 0x1) { - final String name = getResourceName(id); - if (name != null) android.util.Log.d(TAG, "Loading framework drawable #" - + Integer.toHexString(id) + ": " + name - + " at " + file); + if (mPreloading) { + // Preloaded drawables never have a theme, but may be themeable. + final int changingConfigs = cs.getChangingConfigurations(); + if (isColorDrawable) { + if (verifyPreloadConfig(changingConfigs, 0, value.resourceId, "drawable")) { + sPreloadedColorDrawables.put(key, cs); + } + } else { + if (verifyPreloadConfig( + changingConfigs, LAYOUT_DIR_CONFIG, value.resourceId, "drawable")) { + if ((changingConfigs & LAYOUT_DIR_CONFIG) == 0) { + // If this resource does not vary based on layout direction, + // we can put it in all of the preload maps. + sPreloadedDrawables[0].put(key, cs); + sPreloadedDrawables[1].put(key, cs); + } else { + // Otherwise, only in the layout dir we loaded it for. + sPreloadedDrawables[mConfiguration.getLayoutDirection()].put(key, cs); } } + } + } else { + synchronized (mAccessLock) { + final LongSparseArray<WeakReference<ConstantState>> themedCache; + themedCache = caches.getOrCreate(theme == null ? 0 : theme.mThemeResId); + themedCache.put(key, new WeakReference<ConstantState>(cs)); + } + } + } - if (DEBUG_LOAD) Log.v(TAG, "Loading drawable for cookie " - + value.assetCookie + ": " + file); - - if (file.endsWith(".xml")) { - Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file); - try { - XmlResourceParser rp = loadXmlResourceParser( - file, id, value.assetCookie, "drawable"); - dr = Drawable.createFromXml(this, rp); - rp.close(); - } catch (Exception e) { - Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); - NotFoundException rnf = new NotFoundException( - "File " + file + " from drawable resource ID #0x" - + Integer.toHexString(id)); - rnf.initCause(e); - throw rnf; - } - Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); + /** + * Loads a drawable from XML or resources stream. + */ + private Drawable loadDrawableForCookie(TypedValue value, int id, Theme theme) { + if (value.string == null) { + throw new NotFoundException( + "Resource is not a Drawable (color or path): " + value); + } - } else { - Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file); - try { - InputStream is = mAssets.openNonAsset( - value.assetCookie, file, AssetManager.ACCESS_STREAMING); - // System.out.println("Opened file " + file + ": " + is); - dr = Drawable.createFromResourceStream(this, value, is, - file, null); - is.close(); - // System.out.println("Created stream: " + dr); - } catch (Exception e) { - Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); - NotFoundException rnf = new NotFoundException( - "File " + file + " from drawable resource ID #0x" - + Integer.toHexString(id)); - rnf.initCause(e); - throw rnf; - } - Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); + final String file = value.string.toString(); + + if (TRACE_FOR_MISS_PRELOAD) { + // Log only framework resources + if ((id >>> 24) == 0x1) { + final String name = getResourceName(id); + if (name != null) { + Log.d(TAG, "Loading framework drawable #" + Integer.toHexString(id) + + ": " + name + " at " + file); } } } - if (dr != null) { - dr.setChangingConfigurations(value.changingConfigurations); - cs = dr.getConstantState(); - if (cs != null) { - if (mPreloading) { - final int changingConfigs = cs.getChangingConfigurations(); - if (isColorDrawable) { - if (verifyPreloadConfig(changingConfigs, 0, value.resourceId, - "drawable")) { - sPreloadedColorDrawables.put(key, cs); - } - } else { - if (verifyPreloadConfig(changingConfigs, - LAYOUT_DIR_CONFIG, value.resourceId, "drawable")) { - if ((changingConfigs&LAYOUT_DIR_CONFIG) == 0) { - // If this resource does not vary based on layout direction, - // we can put it in all of the preload maps. - sPreloadedDrawables[0].put(key, cs); - sPreloadedDrawables[1].put(key, cs); - } else { - // Otherwise, only in the layout dir we loaded it for. - final LongSparseArray<Drawable.ConstantState> preloads - = sPreloadedDrawables[mConfiguration.getLayoutDirection()]; - preloads.put(key, cs); - } - } - } - } else { - synchronized (mAccessLock) { - //Log.i(TAG, "Saving cached drawable @ #" + - // Integer.toHexString(key.intValue()) - // + " in " + this + ": " + cs); - if (isColorDrawable) { - mColorDrawableCache.put(key, new WeakReference<Drawable.ConstantState>(cs)); - } else { - mDrawableCache.put(key, new WeakReference<Drawable.ConstantState>(cs)); - } - } - } + if (DEBUG_LOAD) { + Log.v(TAG, "Loading drawable for cookie " + value.assetCookie + ": " + file); + } + + final Drawable dr; + + Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file); + try { + if (file.endsWith(".xml")) { + final XmlResourceParser rp = loadXmlResourceParser( + file, id, value.assetCookie, "drawable"); + dr = Drawable.createFromXmlThemed(this, rp, theme); + rp.close(); + } else { + final InputStream is = mAssets.openNonAsset( + value.assetCookie, file, AssetManager.ACCESS_STREAMING); + dr = Drawable.createFromResourceStreamThemed(this, value, is, file, null, theme); + is.close(); } + } catch (Exception e) { + Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); + final NotFoundException rnf = new NotFoundException( + "File " + file + " from drawable resource ID #0x" + Integer.toHexString(id)); + rnf.initCause(e); + throw rnf; } + Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); return dr; } - private Drawable getCachedDrawable( - LongSparseArray<WeakReference<ConstantState>> drawableCache, - long key) { + private Drawable getCachedDrawable(ThemedCaches<ConstantState> caches, long key, Theme theme) { synchronized (mAccessLock) { - WeakReference<Drawable.ConstantState> wr = drawableCache.get(key); - if (wr != null) { // we have the key - Drawable.ConstantState entry = wr.get(); - if (entry != null) { - //Log.i(TAG, "Returning cached drawable @ #" + - // Integer.toHexString(((Integer)key).intValue()) - // + " in " + this + ": " + entry); - return entry.newDrawable(this); - } - else { // our entry has been purged - drawableCache.delete(key); + final int themeKey = theme != null ? theme.mThemeResId : 0; + final LongSparseArray<WeakReference<ConstantState>> themedCache = caches.get(themeKey); + if (themedCache != null) { + final Drawable themedDrawable = getCachedDrawableLocked(themedCache, key); + if (themedDrawable != null) { + return themedDrawable; } } + + // No cached drawable, we'll need to create a new one. + return null; + } + } + + private ConstantState getConstantStateLocked( + LongSparseArray<WeakReference<ConstantState>> drawableCache, long key) { + final WeakReference<ConstantState> wr = drawableCache.get(key); + if (wr != null) { // we have the key + final ConstantState entry = wr.get(); + if (entry != null) { + //Log.i(TAG, "Returning cached drawable @ #" + + // Integer.toHexString(((Integer)key).intValue()) + // + " in " + this + ": " + entry); + return entry; + } else { // our entry has been purged + drawableCache.delete(key); + } + } + return null; + } + + private Drawable getCachedDrawableLocked( + LongSparseArray<WeakReference<ConstantState>> drawableCache, long key) { + final ConstantState entry = getConstantStateLocked(drawableCache, key); + if (entry != null) { + return entry.newDrawable(this); } return null; } @@ -2240,12 +2446,12 @@ public class Resources { "Resource is not a ColorStateList (color or path): " + value); } - String file = value.string.toString(); + final String file = value.string.toString(); if (file.endsWith(".xml")) { Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file); try { - XmlResourceParser rp = loadXmlResourceParser( + final XmlResourceParser rp = loadXmlResourceParser( file, id, value.assetCookie, "colorstatelist"); csl = ColorStateList.createFromXml(this, rp); rp.close(); @@ -2368,37 +2574,12 @@ public class Resources { + Integer.toHexString(id)); } - private TypedArray getCachedStyledAttributes(int len) { + /*package*/ void recycleCachedStyledAttributes(TypedArray attrs) { synchronized (mAccessLock) { - TypedArray attrs = mCachedStyledAttributes; - if (attrs != null) { - mCachedStyledAttributes = null; - if (DEBUG_ATTRIBUTES_CACHE) { - mLastRetrievedAttrs = new RuntimeException("here"); - mLastRetrievedAttrs.fillInStackTrace(); - } - - attrs.mLength = len; - int fullLen = len * AssetManager.STYLE_NUM_ENTRIES; - if (attrs.mData.length >= fullLen) { - return attrs; - } - attrs.mData = new int[fullLen]; - attrs.mIndices = new int[1+len]; - return attrs; - } - if (DEBUG_ATTRIBUTES_CACHE) { - RuntimeException here = new RuntimeException("here"); - here.fillInStackTrace(); - if (mLastRetrievedAttrs != null) { - Log.i(TAG, "Allocated new TypedArray of " + len + " in " + this, here); - Log.i(TAG, "Last retrieved attributes here", mLastRetrievedAttrs); - } - mLastRetrievedAttrs = here; + final TypedArray cached = mCachedStyledAttributes; + if (cached == null || cached.mData.length < attrs.mData.length) { + mCachedStyledAttributes = attrs; } - return new TypedArray(this, - new int[len*AssetManager.STYLE_NUM_ENTRIES], - new int[1+len], len); } } @@ -2412,4 +2593,21 @@ public class Resources { updateConfiguration(null, null); mAssets.ensureStringBlocks(); } + + static class ThemedCaches<T> extends SparseArray<LongSparseArray<WeakReference<T>>> { + /** + * Returns the cache of drawables styled for the specified theme. + * <p> + * Drawables that have themeable attributes but were loaded without + * specifying a theme are cached at themeResId = 0. + */ + public LongSparseArray<WeakReference<T>> getOrCreate(int themeResId) { + LongSparseArray<WeakReference<T>> result = get(themeResId); + if (result == null) { + result = new LongSparseArray<WeakReference<T>>(1); + put(themeResId, result); + } + return result; + } + } } diff --git a/core/java/android/content/res/TypedArray.java b/core/java/android/content/res/TypedArray.java index 83d48aa..15337ce 100644 --- a/core/java/android/content/res/TypedArray.java +++ b/core/java/android/content/res/TypedArray.java @@ -16,11 +16,11 @@ package android.content.res; -import android.content.pm.ActivityInfo; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; +import android.util.Pools.SynchronizedPool; import android.util.TypedValue; import com.android.internal.util.XmlUtils; @@ -32,62 +32,112 @@ import java.util.Arrays; * {@link Resources.Theme#obtainStyledAttributes(AttributeSet, int[], int, int)} * or {@link Resources#obtainAttributes}. Be * sure to call {@link #recycle} when done with them. - * + * * The indices used to retrieve values from this structure correspond to * the positions of the attributes given to obtainStyledAttributes. */ public class TypedArray { - private final Resources mResources; + private static final SynchronizedPool<TypedArray> mPool = new SynchronizedPool<TypedArray>(5); + + static TypedArray obtain(Resources res, int len) { + final TypedArray attrs = mPool.acquire(); + if (attrs != null) { + attrs.mLength = len; + attrs.mResources = res; + attrs.mMetrics = res.getDisplayMetrics(); + attrs.mAssets = res.getAssets(); + attrs.mRecycled = false; + + final int fullLen = len * AssetManager.STYLE_NUM_ENTRIES; + if (attrs.mData.length >= fullLen) { + return attrs; + } + + attrs.mData = new int[fullLen]; + attrs.mIndices = new int[1 + len]; + return attrs; + } + + return new TypedArray(res, + new int[len*AssetManager.STYLE_NUM_ENTRIES], + new int[1+len], len); + } + + private Resources mResources; + private DisplayMetrics mMetrics; + private AssetManager mAssets; + private boolean mRecycled; + /*package*/ XmlBlock.Parser mXml; - /*package*/ int[] mRsrcs; + /*package*/ Resources.Theme mTheme; /*package*/ int[] mData; /*package*/ int[] mIndices; /*package*/ int mLength; /*package*/ TypedValue mValue = new TypedValue(); - + /** * Return the number of values in this array. */ public int length() { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + return mLength; } - + /** * Return the number of indices in the array that actually have data. */ public int getIndexCount() { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + return mIndices[0]; } - + /** * Return an index in the array that has data. - * + * * @param at The index you would like to returned, ranging from 0 to * {@link #getIndexCount()}. - * + * * @return The index at the given offset, which can be used with * {@link #getValue} and related APIs. */ public int getIndex(int at) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + return mIndices[1+at]; } - + /** * Return the Resources object this array was loaded from. */ public Resources getResources() { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + return mResources; } - + /** * Retrieve the styled string value for the attribute at <var>index</var>. - * + * * @param index Index of attribute to retrieve. - * - * @return CharSequence holding string data. May be styled. Returns + * + * @return CharSequence holding string data. May be styled. Returns * null if the attribute is not defined. */ public CharSequence getText(int index) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; final int type = data[index+AssetManager.STYLE_TYPE]; @@ -109,13 +159,17 @@ public class TypedArray { /** * Retrieve the string value for the attribute at <var>index</var>. - * + * * @param index Index of attribute to retrieve. - * + * * @return String holding string data. Any styling information is * removed. Returns null if the attribute is not defined. */ public String getString(int index) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; final int type = data[index+AssetManager.STYLE_TYPE]; @@ -143,14 +197,18 @@ public class TypedArray { * attributes, or conversions from other types. As such, this method * will only return strings for TypedArray objects that come from * attributes in an XML file. - * + * * @param index Index of attribute to retrieve. - * + * * @return String holding string data. Any styling information is * removed. Returns null if the attribute is not defined or is not * an immediate string value. */ public String getNonResourceString(int index) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; final int type = data[index+AssetManager.STYLE_TYPE]; @@ -163,12 +221,12 @@ public class TypedArray { } return null; } - + /** * @hide * Retrieve the string value for the attribute at <var>index</var> that is * not allowed to change with the given configurations. - * + * * @param index Index of attribute to retrieve. * @param allowedChangingConfigs Bit mask of configurations from * {@link Configuration}.NATIVE_CONFIG_* that are allowed to change. @@ -177,6 +235,10 @@ public class TypedArray { * removed. Returns null if the attribute is not defined. */ public String getNonConfigurationString(int index, int allowedChangingConfigs) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; final int type = data[index+AssetManager.STYLE_TYPE]; @@ -202,13 +264,17 @@ public class TypedArray { /** * Retrieve the boolean value for the attribute at <var>index</var>. - * + * * @param index Index of attribute to retrieve. * @param defValue Value to return if the attribute is not defined. - * + * * @return Attribute boolean value, or defValue if not defined. */ public boolean getBoolean(int index, boolean defValue) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; final int type = data[index+AssetManager.STYLE_TYPE]; @@ -232,13 +298,17 @@ public class TypedArray { /** * Retrieve the integer value for the attribute at <var>index</var>. - * + * * @param index Index of attribute to retrieve. * @param defValue Value to return if the attribute is not defined. - * + * * @return Attribute int value, or defValue if not defined. */ public int getInt(int index, int defValue) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; final int type = data[index+AssetManager.STYLE_TYPE]; @@ -262,12 +332,16 @@ public class TypedArray { /** * Retrieve the float value for the attribute at <var>index</var>. - * + * * @param index Index of attribute to retrieve. - * + * * @return Attribute float value, or defValue if not defined.. */ public float getFloat(int index, float defValue) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; final int type = data[index+AssetManager.STYLE_TYPE]; @@ -292,20 +366,24 @@ public class TypedArray { + Integer.toHexString(type)); return defValue; } - + /** * Retrieve the color value for the attribute at <var>index</var>. If * the attribute references a color resource holding a complex * {@link android.content.res.ColorStateList}, then the default color from * the set is returned. - * + * * @param index Index of attribute to retrieve. * @param defValue Value to return if the attribute is not defined or * not a resource. - * + * * @return Attribute color value, or defValue if not defined. */ public int getColor(int index, int defValue) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; final int type = data[index+AssetManager.STYLE_TYPE]; @@ -332,12 +410,16 @@ public class TypedArray { * Retrieve the ColorStateList for the attribute at <var>index</var>. * The value may be either a single solid color or a reference to * a color or complex {@link android.content.res.ColorStateList} description. - * + * * @param index Index of attribute to retrieve. - * + * * @return ColorStateList for the attribute, or null if not defined. */ public ColorStateList getColorStateList(int index) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + final TypedValue value = mValue; if (getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value)) { return mResources.loadColorStateList(value, value.resourceId); @@ -347,14 +429,18 @@ public class TypedArray { /** * Retrieve the integer value for the attribute at <var>index</var>. - * + * * @param index Index of attribute to retrieve. * @param defValue Value to return if the attribute is not defined or * not a resource. - * + * * @return Attribute integer value, or defValue if not defined. */ public int getInteger(int index, int defValue) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; final int type = data[index+AssetManager.STYLE_TYPE]; @@ -370,22 +456,26 @@ public class TypedArray { } /** - * Retrieve a dimensional unit attribute at <var>index</var>. Unit - * conversions are based on the current {@link DisplayMetrics} - * associated with the resources this {@link TypedArray} object - * came from. - * + * Retrieve a dimensional unit attribute at <var>index</var>. Unit + * conversions are based on the current {@link DisplayMetrics} + * associated with the resources this {@link TypedArray} object + * came from. + * * @param index Index of attribute to retrieve. * @param defValue Value to return if the attribute is not defined or * not a resource. - * - * @return Attribute dimension value multiplied by the appropriate + * + * @return Attribute dimension value multiplied by the appropriate * metric, or defValue if not defined. - * + * * @see #getDimensionPixelOffset * @see #getDimensionPixelSize */ public float getDimension(int index, float defValue) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; final int type = data[index+AssetManager.STYLE_TYPE]; @@ -393,7 +483,7 @@ public class TypedArray { return defValue; } else if (type == TypedValue.TYPE_DIMENSION) { return TypedValue.complexToDimension( - data[index+AssetManager.STYLE_DATA], mResources.mMetrics); + data[index+AssetManager.STYLE_DATA], mMetrics); } throw new UnsupportedOperationException("Can't convert to dimension: type=0x" @@ -406,18 +496,22 @@ public class TypedArray { * {@link #getDimension}, except the returned value is converted to * integer pixels for you. An offset conversion involves simply * truncating the base value to an integer. - * + * * @param index Index of attribute to retrieve. * @param defValue Value to return if the attribute is not defined or * not a resource. - * - * @return Attribute dimension value multiplied by the appropriate + * + * @return Attribute dimension value multiplied by the appropriate * metric and truncated to integer pixels, or defValue if not defined. - * + * * @see #getDimension * @see #getDimensionPixelSize */ public int getDimensionPixelOffset(int index, int defValue) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; final int type = data[index+AssetManager.STYLE_TYPE]; @@ -425,7 +519,7 @@ public class TypedArray { return defValue; } else if (type == TypedValue.TYPE_DIMENSION) { return TypedValue.complexToDimensionPixelOffset( - data[index+AssetManager.STYLE_DATA], mResources.mMetrics); + data[index+AssetManager.STYLE_DATA], mMetrics); } throw new UnsupportedOperationException("Can't convert to dimension: type=0x" @@ -439,18 +533,22 @@ public class TypedArray { * integer pixels for use as a size. A size conversion involves * rounding the base value, and ensuring that a non-zero base value * is at least one pixel in size. - * + * * @param index Index of attribute to retrieve. * @param defValue Value to return if the attribute is not defined or * not a resource. - * - * @return Attribute dimension value multiplied by the appropriate + * + * @return Attribute dimension value multiplied by the appropriate * metric and truncated to integer pixels, or defValue if not defined. - * + * * @see #getDimension * @see #getDimensionPixelOffset */ public int getDimensionPixelSize(int index, int defValue) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; final int type = data[index+AssetManager.STYLE_TYPE]; @@ -458,7 +556,7 @@ public class TypedArray { return defValue; } else if (type == TypedValue.TYPE_DIMENSION) { return TypedValue.complexToDimensionPixelSize( - data[index+AssetManager.STYLE_DATA], mResources.mMetrics); + data[index+AssetManager.STYLE_DATA], mMetrics); } throw new UnsupportedOperationException("Can't convert to dimension: type=0x" @@ -470,14 +568,18 @@ public class TypedArray { * {@link android.view.ViewGroup}'s layout_width and layout_height * attributes. This is only here for performance reasons; applications * should use {@link #getDimensionPixelSize}. - * + * * @param index Index of the attribute to retrieve. * @param name Textual name of attribute for error reporting. - * - * @return Attribute dimension value multiplied by the appropriate + * + * @return Attribute dimension value multiplied by the appropriate * metric and truncated to integer pixels. */ public int getLayoutDimension(int index, String name) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; final int type = data[index+AssetManager.STYLE_TYPE]; @@ -486,27 +588,31 @@ public class TypedArray { return data[index+AssetManager.STYLE_DATA]; } else if (type == TypedValue.TYPE_DIMENSION) { return TypedValue.complexToDimensionPixelSize( - data[index+AssetManager.STYLE_DATA], mResources.mMetrics); + data[index+AssetManager.STYLE_DATA], mMetrics); } throw new RuntimeException(getPositionDescription() + ": You must supply a " + name + " attribute."); } - + /** * Special version of {@link #getDimensionPixelSize} for retrieving * {@link android.view.ViewGroup}'s layout_width and layout_height * attributes. This is only here for performance reasons; applications * should use {@link #getDimensionPixelSize}. - * + * * @param index Index of the attribute to retrieve. * @param defValue The default value to return if this attribute is not * default or contains the wrong type of data. - * - * @return Attribute dimension value multiplied by the appropriate + * + * @return Attribute dimension value multiplied by the appropriate * metric and truncated to integer pixels. */ public int getLayoutDimension(int index, int defValue) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; final int type = data[index+AssetManager.STYLE_TYPE]; @@ -515,7 +621,7 @@ public class TypedArray { return data[index+AssetManager.STYLE_DATA]; } else if (type == TypedValue.TYPE_DIMENSION) { return TypedValue.complexToDimensionPixelSize( - data[index+AssetManager.STYLE_DATA], mResources.mMetrics); + data[index+AssetManager.STYLE_DATA], mMetrics); } return defValue; @@ -523,20 +629,24 @@ public class TypedArray { /** * Retrieve a fractional unit attribute at <var>index</var>. - * - * @param index Index of attribute to retrieve. - * @param base The base value of this fraction. In other words, a + * + * @param index Index of attribute to retrieve. + * @param base The base value of this fraction. In other words, a * standard fraction is multiplied by this value. - * @param pbase The parent base value of this fraction. In other + * @param pbase The parent base value of this fraction. In other * words, a parent fraction (nn%p) is multiplied by this * value. * @param defValue Value to return if the attribute is not defined or * not a resource. - * - * @return Attribute fractional value multiplied by the appropriate - * base value, or defValue if not defined. + * + * @return Attribute fractional value multiplied by the appropriate + * base value, or defValue if not defined. */ public float getFraction(int index, int base, int pbase, float defValue) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; final int type = data[index+AssetManager.STYLE_TYPE]; @@ -553,19 +663,23 @@ public class TypedArray { /** * Retrieve the resource identifier for the attribute at - * <var>index</var>. Note that attribute resource as resolved when - * the overall {@link TypedArray} object is retrieved. As a - * result, this function will return the resource identifier of the - * final resource value that was found, <em>not</em> necessarily the - * original resource that was specified by the attribute. - * + * <var>index</var>. Note that attribute resource as resolved when + * the overall {@link TypedArray} object is retrieved. As a + * result, this function will return the resource identifier of the + * final resource value that was found, <em>not</em> necessarily the + * original resource that was specified by the attribute. + * * @param index Index of attribute to retrieve. * @param defValue Value to return if the attribute is not defined or * not a resource. - * + * * @return Attribute resource identifier, or defValue if not defined. */ public int getResourceId(int index, int defValue) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; if (data[index+AssetManager.STYLE_TYPE] != TypedValue.TYPE_NULL) { @@ -578,16 +692,43 @@ public class TypedArray { } /** + * Retrieve the theme attribute resource identifier for the attribute at + * <var>index</var>. + * + * @param index Index of attribute to retrieve. + * @param defValue Value to return if the attribute is not defined or not a + * resource. + * @return Theme attribute resource identifier, or defValue if not defined. + * @hide + */ + public int getThemeAttributeId(int index, int defValue) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + + index *= AssetManager.STYLE_NUM_ENTRIES; + final int[] data = mData; + if (data[index + AssetManager.STYLE_TYPE] == TypedValue.TYPE_ATTRIBUTE) { + return data[index + AssetManager.STYLE_DATA]; + } + return defValue; + } + + /** * Retrieve the Drawable for the attribute at <var>index</var>. This * gets the resource ID of the selected attribute, and uses * {@link Resources#getDrawable Resources.getDrawable} of the owning * Resources object to retrieve its Drawable. - * + * * @param index Index of attribute to retrieve. - * + * * @return Drawable for the attribute, or null if not defined. */ public Drawable getDrawable(int index) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + final TypedValue value = mValue; if (getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value)) { if (false) { @@ -599,7 +740,7 @@ public class TypedArray { + " cookie=" + value.assetCookie); System.out.println("******************************************************************"); } - return mResources.loadDrawable(value, value.resourceId); + return mResources.loadDrawable(value, value.resourceId, mTheme); } return null; } @@ -609,12 +750,16 @@ public class TypedArray { * This gets the resource ID of the selected attribute, and uses * {@link Resources#getTextArray Resources.getTextArray} of the owning * Resources object to retrieve its String[]. - * + * * @param index Index of attribute to retrieve. - * + * * @return CharSequence[] for the attribute, or null if not defined. */ public CharSequence[] getTextArray(int index) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + final TypedValue value = mValue; if (getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value)) { if (false) { @@ -633,43 +778,70 @@ public class TypedArray { /** * Retrieve the raw TypedValue for the attribute at <var>index</var>. - * + * * @param index Index of attribute to retrieve. * @param outValue TypedValue object in which to place the attribute's * data. - * - * @return Returns true if the value was retrieved, else false. + * + * @return Returns true if the value was retrieved, else false. */ public boolean getValue(int index, TypedValue outValue) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + return getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, outValue); } /** + * Returns the type of attribute at the specified index. + * + * @param index Index of attribute whose type to retrieve. + * @return Attribute type. + */ + public int getType(int index) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + + index *= AssetManager.STYLE_NUM_ENTRIES; + return mData[index + AssetManager.STYLE_TYPE]; + } + + /** * Determines whether there is an attribute at <var>index</var>. - * + * * @param index Index of attribute to retrieve. - * + * * @return True if the attribute has a value, false otherwise. */ public boolean hasValue(int index) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + index *= AssetManager.STYLE_NUM_ENTRIES; final int[] data = mData; final int type = data[index+AssetManager.STYLE_TYPE]; return type != TypedValue.TYPE_NULL; } - + /** - * Retrieve the raw TypedValue for the attribute at <var>index</var> - * and return a temporary object holding its data. This object is only - * valid until the next call on to {@link TypedArray}. - * + * Retrieve the raw TypedValue for the attribute at <var>index</var> + * and return a temporary object holding its data. This object is only + * valid until the next call on to {@link TypedArray}. + * * @param index Index of attribute to retrieve. - * - * @return Returns a TypedValue object if the attribute is defined, + * + * @return Returns a TypedValue object if the attribute is defined, * containing its data; otherwise returns null. (You will not * receive a TypedValue whose type is TYPE_NULL.) */ public TypedValue peekValue(int index) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + final TypedValue value = mValue; if (getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value)) { return value; @@ -681,20 +853,66 @@ public class TypedArray { * Returns a message about the parser state suitable for printing error messages. */ public String getPositionDescription() { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + return mXml != null ? mXml.getPositionDescription() : "<internal>"; } /** - * Give back a previously retrieved array, for later re-use. + * Recycle the TypedArray, to be re-used by a later caller. After calling + * this function you must not ever touch the typed array again. */ public void recycle() { - synchronized (mResources.mAccessLock) { - TypedArray cached = mResources.mCachedStyledAttributes; - if (cached == null || cached.mData.length < mData.length) { - mXml = null; - mResources.mCachedStyledAttributes = this; + if (mRecycled) { + throw new RuntimeException(toString() + " recycled twice!"); + } + + mRecycled = true; + mResources = null; + mMetrics = null; + mAssets = null; + + // These may have been set by the client. + mXml = null; + mTheme = null; + + synchronized (mPool) { + mPool.release(this); + } + } + + /** + * Extracts theme attributes from a typed array for later resolution using + * {@link Theme#resolveAttributes(int[], int[], int, int)}. + * + * @param array An array to populate with theme attributes. If the array is + * null or not large enough, a new array will be returned. + * @return an array of length {@link #getIndexCount()} populated with theme + * attributes, or null if there are no theme attributes in the + * typed array + * @hide + */ + public int[] extractThemeAttrs() { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + + int[] attrs = null; + + final int N = length(); + for (int i = 0; i < N; i++) { + final int attrId = getThemeAttributeId(i, 0); + if (attrId != 0) { + if (attrs == null) { + attrs = new int[N]; + } + attrs[i] = attrId; } } + + return attrs; } private boolean getValueAt(int index, TypedValue outValue) { @@ -723,18 +941,19 @@ public class TypedArray { } return null; } - //System.out.println("Getting pooled from: " + v); - return mResources.mAssets.getPooledString( - cookie, data[index+AssetManager.STYLE_DATA]); + return mAssets.getPooledStringForCookie(cookie, data[index+AssetManager.STYLE_DATA]); } /*package*/ TypedArray(Resources resources, int[] data, int[] indices, int len) { mResources = resources; + mMetrics = mResources.getDisplayMetrics(); + mAssets = mResources.getAssets(); mData = data; mIndices = indices; mLength = len; } + @Override public String toString() { return Arrays.toString(mData); } diff --git a/core/java/android/content/res/XmlBlock.java b/core/java/android/content/res/XmlBlock.java index 3ad357f..2f4d69b 100644 --- a/core/java/android/content/res/XmlBlock.java +++ b/core/java/android/content/res/XmlBlock.java @@ -375,7 +375,7 @@ final class XmlBlock { boolean defaultValue) { int t = nativeGetAttributeDataType(mParseState, idx); // Note: don't attempt to convert any other types, because - // we want to count on appt doing the conversion for us. + // we want to count on aapt doing the conversion for us. if (t >= TypedValue.TYPE_FIRST_INT && t <= TypedValue.TYPE_LAST_INT) { return nativeGetAttributeData(mParseState, idx) != 0; @@ -385,7 +385,7 @@ final class XmlBlock { public int getAttributeResourceValue(int idx, int defaultValue) { int t = nativeGetAttributeDataType(mParseState, idx); // Note: don't attempt to convert any other types, because - // we want to count on appt doing the conversion for us. + // we want to count on aapt doing the conversion for us. if (t == TypedValue.TYPE_REFERENCE) { return nativeGetAttributeData(mParseState, idx); } @@ -394,7 +394,7 @@ final class XmlBlock { public int getAttributeIntValue(int idx, int defaultValue) { int t = nativeGetAttributeDataType(mParseState, idx); // Note: don't attempt to convert any other types, because - // we want to count on appt doing the conversion for us. + // we want to count on aapt doing the conversion for us. if (t >= TypedValue.TYPE_FIRST_INT && t <= TypedValue.TYPE_LAST_INT) { return nativeGetAttributeData(mParseState, idx); @@ -404,7 +404,7 @@ final class XmlBlock { public int getAttributeUnsignedIntValue(int idx, int defaultValue) { int t = nativeGetAttributeDataType(mParseState, idx); // Note: don't attempt to convert any other types, because - // we want to count on appt doing the conversion for us. + // we want to count on aapt doing the conversion for us. if (t >= TypedValue.TYPE_FIRST_INT && t <= TypedValue.TYPE_LAST_INT) { return nativeGetAttributeData(mParseState, idx); @@ -414,7 +414,7 @@ final class XmlBlock { public float getAttributeFloatValue(int idx, float defaultValue) { int t = nativeGetAttributeDataType(mParseState, idx); // Note: don't attempt to convert any other types, because - // we want to count on appt doing the conversion for us. + // we want to count on aapt doing the conversion for us. if (t == TypedValue.TYPE_FLOAT) { return Float.intBitsToFloat( nativeGetAttributeData(mParseState, idx)); diff --git a/core/java/android/database/CursorToBulkCursorAdaptor.java b/core/java/android/database/CursorToBulkCursorAdaptor.java index 82a61d4..7dcfae2 100644 --- a/core/java/android/database/CursorToBulkCursorAdaptor.java +++ b/core/java/android/database/CursorToBulkCursorAdaptor.java @@ -20,7 +20,6 @@ import android.net.Uri; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; -import android.util.Log; /** diff --git a/core/java/android/database/sqlite/SQLiteOpenHelper.java b/core/java/android/database/sqlite/SQLiteOpenHelper.java index 431eca2..2dd4800 100644 --- a/core/java/android/database/sqlite/SQLiteOpenHelper.java +++ b/core/java/android/database/sqlite/SQLiteOpenHelper.java @@ -18,7 +18,6 @@ package android.database.sqlite; import android.content.Context; import android.database.DatabaseErrorHandler; -import android.database.DefaultDatabaseErrorHandler; import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.util.Log; diff --git a/core/java/android/ddm/DdmHandleNativeHeap.java b/core/java/android/ddm/DdmHandleNativeHeap.java index 6bd65aa..775c570 100644 --- a/core/java/android/ddm/DdmHandleNativeHeap.java +++ b/core/java/android/ddm/DdmHandleNativeHeap.java @@ -20,7 +20,6 @@ import org.apache.harmony.dalvik.ddmc.Chunk; import org.apache.harmony.dalvik.ddmc.ChunkHandler; import org.apache.harmony.dalvik.ddmc.DdmServer; import android.util.Log; -import java.nio.ByteBuffer; /** * Handle thread-related traffic. diff --git a/core/java/android/ddm/DdmHandleProfiling.java b/core/java/android/ddm/DdmHandleProfiling.java index 537763d..cce4dd2 100644 --- a/core/java/android/ddm/DdmHandleProfiling.java +++ b/core/java/android/ddm/DdmHandleProfiling.java @@ -21,7 +21,6 @@ import org.apache.harmony.dalvik.ddmc.ChunkHandler; import org.apache.harmony.dalvik.ddmc.DdmServer; import android.os.Debug; import android.util.Log; -import java.io.IOException; import java.nio.ByteBuffer; /** diff --git a/core/java/android/gesture/GestureOverlayView.java b/core/java/android/gesture/GestureOverlayView.java index 2d47f28..6e3a00f 100644 --- a/core/java/android/gesture/GestureOverlayView.java +++ b/core/java/android/gesture/GestureOverlayView.java @@ -134,11 +134,16 @@ public class GestureOverlayView extends FrameLayout { this(context, attrs, com.android.internal.R.attr.gestureOverlayViewStyle); } - public GestureOverlayView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public GestureOverlayView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public GestureOverlayView( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = context.obtainStyledAttributes(attrs, - R.styleable.GestureOverlayView, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.GestureOverlayView, defStyleAttr, defStyleRes); mGestureStrokeWidth = a.getFloat(R.styleable.GestureOverlayView_gestureStrokeWidth, mGestureStrokeWidth); diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java index eae4a46..35c86e7 100644 --- a/core/java/android/hardware/Camera.java +++ b/core/java/android/hardware/Camera.java @@ -47,7 +47,6 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; -import java.util.concurrent.locks.ReentrantLock; /** * The Camera class is used to set image capture settings, start/stop preview, diff --git a/core/java/android/hardware/GeomagneticField.java b/core/java/android/hardware/GeomagneticField.java index 0369825..ef05732 100644 --- a/core/java/android/hardware/GeomagneticField.java +++ b/core/java/android/hardware/GeomagneticField.java @@ -361,7 +361,7 @@ public class GeomagneticField { mP[0] = new float[] { 1.0f }; mPDeriv[0] = new float[] { 0.0f }; for (int n = 1; n <= maxN; n++) { - mP[n] = new float[n + 1]; + mP[n] = new float[n + 1]; mPDeriv[n] = new float[n + 1]; for (int m = 0; m <= n; m++) { if (n == m) { diff --git a/core/java/android/hardware/ICameraService.aidl b/core/java/android/hardware/ICameraService.aidl index 542af6a..4c50dda 100644 --- a/core/java/android/hardware/ICameraService.aidl +++ b/core/java/android/hardware/ICameraService.aidl @@ -61,4 +61,12 @@ interface ICameraService int removeListener(ICameraServiceListener listener); int getCameraCharacteristics(int cameraId, out CameraMetadataNative info); + + /** + * The java stubs for this method are not intended to be used. Please use + * the native stub in frameworks/av/include/camera/ICameraService.h instead. + * The BinderHolder output is being used as a placeholder, and will not be + * well-formatted in the generated java method. + */ + int getCameraVendorTagDescriptor(out BinderHolder desc); } diff --git a/core/java/android/hardware/SerialManager.java b/core/java/android/hardware/SerialManager.java index c5e1c2b..e0680bf 100644 --- a/core/java/android/hardware/SerialManager.java +++ b/core/java/android/hardware/SerialManager.java @@ -17,16 +17,12 @@ package android.hardware; -import android.app.PendingIntent; import android.content.Context; -import android.os.Bundle; import android.os.ParcelFileDescriptor; import android.os.RemoteException; -import android.os.SystemProperties; import android.util.Log; import java.io.IOException; -import java.util.HashMap; /** * @hide diff --git a/core/java/android/hardware/SerialPort.java b/core/java/android/hardware/SerialPort.java index f50cdef..5d83d9c 100644 --- a/core/java/android/hardware/SerialPort.java +++ b/core/java/android/hardware/SerialPort.java @@ -17,14 +17,9 @@ package android.hardware; import android.os.ParcelFileDescriptor; -import android.util.Log; import java.io.FileDescriptor; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.InputStream; import java.io.IOException; -import java.io.OutputStream; import java.nio.ByteBuffer; diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java index a38beec..9852776 100644 --- a/core/java/android/hardware/camera2/CameraCharacteristics.java +++ b/core/java/android/hardware/camera2/CameraCharacteristics.java @@ -126,206 +126,300 @@ public final class CameraCharacteristics extends CameraMetadata { * modify the comment blocks at the start or end. *~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~*/ + /** - * <p> - * Which set of antibanding modes are - * supported - * </p> + * <p>The set of auto-exposure antibanding modes that are + * supported by this camera device.</p> + * <p>Not all of the auto-exposure anti-banding modes may be + * supported by a given camera device. This field lists the + * valid anti-banding modes that the application may request + * for this camera device; they must include AUTO.</p> */ public static final Key<byte[]> CONTROL_AE_AVAILABLE_ANTIBANDING_MODES = new Key<byte[]>("android.control.aeAvailableAntibandingModes", byte[].class); /** - * <p> - * List of frame rate ranges supported by the - * AE algorithm/hardware - * </p> + * <p>The set of auto-exposure modes that are supported by this + * camera device.</p> + * <p>Not all the auto-exposure modes may be supported by a + * given camera device, especially if no flash unit is + * available. This entry lists the valid modes for + * {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} for this camera device.</p> + * <p>All camera devices support ON, and all camera devices with + * flash units support ON_AUTO_FLASH and + * ON_ALWAYS_FLASH.</p> + * <p>FULL mode camera devices always support OFF mode, + * which enables application control of camera exposure time, + * sensitivity, and frame duration.</p> + * + * @see CaptureRequest#CONTROL_AE_MODE + */ + public static final Key<byte[]> CONTROL_AE_AVAILABLE_MODES = + new Key<byte[]>("android.control.aeAvailableModes", byte[].class); + + /** + * <p>List of frame rate ranges supported by the + * AE algorithm/hardware</p> */ public static final Key<int[]> CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES = new Key<int[]>("android.control.aeAvailableTargetFpsRanges", int[].class); /** - * <p> - * Maximum and minimum exposure compensation + * <p>Maximum and minimum exposure compensation * setting, in counts of - * android.control.aeCompensationStepSize - * </p> + * {@link CameraCharacteristics#CONTROL_AE_COMPENSATION_STEP android.control.aeCompensationStep}.</p> + * + * @see CameraCharacteristics#CONTROL_AE_COMPENSATION_STEP */ public static final Key<int[]> CONTROL_AE_COMPENSATION_RANGE = new Key<int[]>("android.control.aeCompensationRange", int[].class); /** - * <p> - * Smallest step by which exposure compensation - * can be changed - * </p> + * <p>Smallest step by which exposure compensation + * can be changed</p> */ public static final Key<Rational> CONTROL_AE_COMPENSATION_STEP = new Key<Rational>("android.control.aeCompensationStep", Rational.class); /** - * <p> - * List of AF modes that can be - * selected - * </p> + * <p>List of AF modes that can be + * selected with {@link CaptureRequest#CONTROL_AF_MODE android.control.afMode}.</p> + * <p>Not all the auto-focus modes may be supported by a + * given camera device. This entry lists the valid modes for + * {@link CaptureRequest#CONTROL_AF_MODE android.control.afMode} for this camera device.</p> + * <p>All camera devices will support OFF mode, and all camera devices with + * adjustable focuser units (<code>{@link CameraCharacteristics#LENS_INFO_MINIMUM_FOCUS_DISTANCE android.lens.info.minimumFocusDistance} > 0</code>) + * will support AUTO mode.</p> + * + * @see CaptureRequest#CONTROL_AF_MODE + * @see CameraCharacteristics#LENS_INFO_MINIMUM_FOCUS_DISTANCE */ public static final Key<byte[]> CONTROL_AF_AVAILABLE_MODES = new Key<byte[]>("android.control.afAvailableModes", byte[].class); /** - * <p> - * what subset of the full color effect enum - * list is supported - * </p> + * <p>List containing the subset of color effects + * specified in {@link CaptureRequest#CONTROL_EFFECT_MODE android.control.effectMode} that is supported by + * this device.</p> + * <p>This list contains the color effect modes that can be applied to + * images produced by the camera device. Only modes that have + * been fully implemented for the current device may be included here. + * Implementations are not expected to be consistent across all devices. + * If no color effect modes are available for a device, this should + * simply be set to OFF.</p> + * <p>A color effect will only be applied if + * {@link CaptureRequest#CONTROL_MODE android.control.mode} != OFF.</p> + * + * @see CaptureRequest#CONTROL_EFFECT_MODE + * @see CaptureRequest#CONTROL_MODE */ public static final Key<byte[]> CONTROL_AVAILABLE_EFFECTS = new Key<byte[]>("android.control.availableEffects", byte[].class); /** - * <p> - * what subset of the scene mode enum list is - * supported. - * </p> + * <p>List containing a subset of scene modes + * specified in {@link CaptureRequest#CONTROL_SCENE_MODE android.control.sceneMode}.</p> + * <p>This list contains scene modes that can be set for the camera device. + * Only scene modes that have been fully implemented for the + * camera device may be included here. Implementations are not expected + * to be consistent across all devices. If no scene modes are supported + * by the camera device, this will be set to <code>[DISABLED]</code>.</p> + * + * @see CaptureRequest#CONTROL_SCENE_MODE */ public static final Key<byte[]> CONTROL_AVAILABLE_SCENE_MODES = new Key<byte[]>("android.control.availableSceneModes", byte[].class); /** - * <p> - * List of video stabilization modes that can - * be supported - * </p> + * <p>List of video stabilization modes that can + * be supported</p> */ public static final Key<byte[]> CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES = new Key<byte[]>("android.control.availableVideoStabilizationModes", byte[].class); /** + * <p>The set of auto-white-balance modes ({@link CaptureRequest#CONTROL_AWB_MODE android.control.awbMode}) + * that are supported by this camera device.</p> + * <p>Not all the auto-white-balance modes may be supported by a + * given camera device. This entry lists the valid modes for + * {@link CaptureRequest#CONTROL_AWB_MODE android.control.awbMode} for this camera device.</p> + * <p>All camera devices will support ON mode.</p> + * <p>FULL mode camera devices will always support OFF mode, + * which enables application control of white balance, by using + * {@link CaptureRequest#COLOR_CORRECTION_TRANSFORM android.colorCorrection.transform} and {@link CaptureRequest#COLOR_CORRECTION_GAINS android.colorCorrection.gains}({@link CaptureRequest#COLOR_CORRECTION_MODE android.colorCorrection.mode} must be set to TRANSFORM_MATRIX).</p> + * + * @see CaptureRequest#COLOR_CORRECTION_GAINS + * @see CaptureRequest#COLOR_CORRECTION_MODE + * @see CaptureRequest#COLOR_CORRECTION_TRANSFORM + * @see CaptureRequest#CONTROL_AWB_MODE */ public static final Key<byte[]> CONTROL_AWB_AVAILABLE_MODES = new Key<byte[]>("android.control.awbAvailableModes", byte[].class); /** - * <p> - * For AE, AWB, and AF, how many individual - * regions can be listed for metering? - * </p> + * <p>List of the maximum number of regions that can be used for metering in + * auto-exposure (AE), auto-white balance (AWB), and auto-focus (AF); + * this corresponds to the the maximum number of elements in + * {@link CaptureRequest#CONTROL_AE_REGIONS android.control.aeRegions}, {@link CaptureRequest#CONTROL_AWB_REGIONS android.control.awbRegions}, + * and {@link CaptureRequest#CONTROL_AF_REGIONS android.control.afRegions}.</p> + * + * @see CaptureRequest#CONTROL_AE_REGIONS + * @see CaptureRequest#CONTROL_AF_REGIONS + * @see CaptureRequest#CONTROL_AWB_REGIONS + */ + public static final Key<int[]> CONTROL_MAX_REGIONS = + new Key<int[]>("android.control.maxRegions", int[].class); + + /** + * <p>The set of edge enhancement modes supported by this camera device.</p> + * <p>This tag lists the valid modes for {@link CaptureRequest#EDGE_MODE android.edge.mode}.</p> + * <p>Full-capability camera devices must always support OFF and FAST.</p> + * + * @see CaptureRequest#EDGE_MODE + */ + public static final Key<byte[]> EDGE_AVAILABLE_EDGE_MODES = + new Key<byte[]>("android.edge.availableEdgeModes", byte[].class); + + /** + * <p>Whether this camera device has a + * flash.</p> + * <p>If no flash, none of the flash controls do + * anything. All other metadata should return 0.</p> */ - public static final Key<Integer> CONTROL_MAX_REGIONS = - new Key<Integer>("android.control.maxRegions", int.class); + public static final Key<Boolean> FLASH_INFO_AVAILABLE = + new Key<Boolean>("android.flash.info.available", boolean.class); /** - * <p> - * Whether this camera has a - * flash - * </p> - * <p> - * If no flash, none of the flash controls do - * anything. All other metadata should return 0 - * </p> + * <p>The set of hot pixel correction modes that are supported by this + * camera device.</p> + * <p>This tag lists valid modes for {@link CaptureRequest#HOT_PIXEL_MODE android.hotPixel.mode}.</p> + * <p>FULL mode camera devices will always support FAST.</p> + * + * @see CaptureRequest#HOT_PIXEL_MODE */ - public static final Key<Byte> FLASH_INFO_AVAILABLE = - new Key<Byte>("android.flash.info.available", byte.class); + public static final Key<byte[]> HOT_PIXEL_AVAILABLE_HOT_PIXEL_MODES = + new Key<byte[]>("android.hotPixel.availableHotPixelModes", byte[].class); /** - * <p> - * Supported resolutions for the JPEG - * thumbnail - * </p> + * <p>Supported resolutions for the JPEG thumbnail</p> + * <p>Below condiditions will be satisfied for this size list:</p> + * <ul> + * <li>The sizes will be sorted by increasing pixel area (width x height). + * If several resolutions have the same area, they will be sorted by increasing width.</li> + * <li>The aspect ratio of the largest thumbnail size will be same as the + * aspect ratio of largest JPEG output size in {@link CameraCharacteristics#SCALER_AVAILABLE_STREAM_CONFIGURATIONS android.scaler.availableStreamConfigurations}. + * The largest size is defined as the size that has the largest pixel area + * in a given size list.</li> + * <li>Each output JPEG size in {@link CameraCharacteristics#SCALER_AVAILABLE_STREAM_CONFIGURATIONS android.scaler.availableStreamConfigurations} will have at least + * one corresponding size that has the same aspect ratio in availableThumbnailSizes, + * and vice versa.</li> + * <li>All non (0, 0) sizes will have non-zero widths and heights.</li> + * </ul> + * + * @see CameraCharacteristics#SCALER_AVAILABLE_STREAM_CONFIGURATIONS */ public static final Key<android.hardware.camera2.Size[]> JPEG_AVAILABLE_THUMBNAIL_SIZES = new Key<android.hardware.camera2.Size[]>("android.jpeg.availableThumbnailSizes", android.hardware.camera2.Size[].class); /** - * <p> - * List of supported aperture - * values - * </p> - * <p> - * If variable aperture not available, only setting - * should be for the fixed aperture - * </p> + * <p>List of supported aperture + * values.</p> + * <p>If the camera device doesn't support variable apertures, + * listed value will be the fixed aperture.</p> + * <p>If the camera device supports variable apertures, the aperture value + * in this list will be sorted in ascending order.</p> */ public static final Key<float[]> LENS_INFO_AVAILABLE_APERTURES = new Key<float[]>("android.lens.info.availableApertures", float[].class); /** - * <p> - * List of supported ND filter - * values - * </p> - * <p> - * If not available, only setting is 0. Otherwise, - * lists the available exposure index values for dimming - * (2 would mean the filter is set to reduce incoming - * light by two stops) - * </p> + * <p>List of supported neutral density filter values for + * {@link CaptureRequest#LENS_FILTER_DENSITY android.lens.filterDensity}.</p> + * <p>If changing {@link CaptureRequest#LENS_FILTER_DENSITY android.lens.filterDensity} is not supported, + * availableFilterDensities must contain only 0. Otherwise, this + * list contains only the exact filter density values available on + * this camera device.</p> + * + * @see CaptureRequest#LENS_FILTER_DENSITY */ public static final Key<float[]> LENS_INFO_AVAILABLE_FILTER_DENSITIES = new Key<float[]>("android.lens.info.availableFilterDensities", float[].class); /** - * <p> - * If fitted with optical zoom, what focal - * lengths are available. If not, the static focal - * length - * </p> - * <p> - * If optical zoom not supported, only one value - * should be reported - * </p> + * <p>The available focal lengths for this device for use with + * {@link CaptureRequest#LENS_FOCAL_LENGTH android.lens.focalLength}.</p> + * <p>If optical zoom is not supported, this will only report + * a single value corresponding to the static focal length of the + * device. Otherwise, this will report every focal length supported + * by the device.</p> + * + * @see CaptureRequest#LENS_FOCAL_LENGTH */ public static final Key<float[]> LENS_INFO_AVAILABLE_FOCAL_LENGTHS = new Key<float[]>("android.lens.info.availableFocalLengths", float[].class); /** - * <p> - * List of supported optical image - * stabilization modes - * </p> + * <p>List containing a subset of the optical image + * stabilization (OIS) modes specified in + * {@link CaptureRequest#LENS_OPTICAL_STABILIZATION_MODE android.lens.opticalStabilizationMode}.</p> + * <p>If OIS is not implemented for a given camera device, this should + * contain only OFF.</p> + * + * @see CaptureRequest#LENS_OPTICAL_STABILIZATION_MODE */ public static final Key<byte[]> LENS_INFO_AVAILABLE_OPTICAL_STABILIZATION = new Key<byte[]>("android.lens.info.availableOpticalStabilization", byte[].class); /** - * <p> - * Hyperfocal distance for this lens; set to - * 0 if fixed focus - * </p> - * <p> - * The hyperfocal distance is used for the old - * API's 'fixed' setting - * </p> + * <p>Optional. Hyperfocal distance for this lens.</p> + * <p>If the lens is fixed focus, the camera device will report 0.</p> + * <p>If the lens is not fixed focus, the camera device will report this + * field when {@link CameraCharacteristics#LENS_INFO_FOCUS_DISTANCE_CALIBRATION android.lens.info.focusDistanceCalibration} is APPROXIMATE or CALIBRATED.</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + * + * @see CameraCharacteristics#LENS_INFO_FOCUS_DISTANCE_CALIBRATION */ public static final Key<Float> LENS_INFO_HYPERFOCAL_DISTANCE = new Key<Float>("android.lens.info.hyperfocalDistance", float.class); /** - * <p> - * Shortest distance from frontmost surface - * of the lens that can be focused correctly - * </p> - * <p> - * If the lens is fixed-focus, this should be - * 0 - * </p> + * <p>Shortest distance from frontmost surface + * of the lens that can be focused correctly.</p> + * <p>If the lens is fixed-focus, this should be + * 0.</p> */ public static final Key<Float> LENS_INFO_MINIMUM_FOCUS_DISTANCE = new Key<Float>("android.lens.info.minimumFocusDistance", float.class); /** - * <p> - * Dimensions of lens shading - * map - * </p> + * <p>Dimensions of lens shading map.</p> + * <p>The map should be on the order of 30-40 rows and columns, and + * must be smaller than 64x64.</p> */ public static final Key<android.hardware.camera2.Size> LENS_INFO_SHADING_MAP_SIZE = new Key<android.hardware.camera2.Size>("android.lens.info.shadingMapSize", android.hardware.camera2.Size.class); /** - * <p> - * Direction the camera faces relative to - * device screen - * </p> + * <p>The lens focus distance calibration quality.</p> + * <p>The lens focus distance calibration quality determines the reliability of + * focus related metadata entries, i.e. {@link CaptureRequest#LENS_FOCUS_DISTANCE android.lens.focusDistance}, + * {@link CaptureResult#LENS_FOCUS_RANGE android.lens.focusRange}, {@link CameraCharacteristics#LENS_INFO_HYPERFOCAL_DISTANCE android.lens.info.hyperfocalDistance}, and + * {@link CameraCharacteristics#LENS_INFO_MINIMUM_FOCUS_DISTANCE android.lens.info.minimumFocusDistance}.</p> + * + * @see CaptureRequest#LENS_FOCUS_DISTANCE + * @see CaptureResult#LENS_FOCUS_RANGE + * @see CameraCharacteristics#LENS_INFO_HYPERFOCAL_DISTANCE + * @see CameraCharacteristics#LENS_INFO_MINIMUM_FOCUS_DISTANCE + * @see #LENS_INFO_FOCUS_DISTANCE_CALIBRATION_UNCALIBRATED + * @see #LENS_INFO_FOCUS_DISTANCE_CALIBRATION_APPROXIMATE + * @see #LENS_INFO_FOCUS_DISTANCE_CALIBRATION_CALIBRATED + */ + public static final Key<Integer> LENS_INFO_FOCUS_DISTANCE_CALIBRATION = + new Key<Integer>("android.lens.info.focusDistanceCalibration", int.class); + + /** + * <p>Direction the camera faces relative to + * device screen</p> * @see #LENS_FACING_FRONT * @see #LENS_FACING_BACK */ @@ -333,292 +427,819 @@ public final class CameraCharacteristics extends CameraMetadata { new Key<Integer>("android.lens.facing", int.class); /** - * <p> - * If set to 1, the HAL will always split result + * <p>The set of noise reduction modes supported by this camera device.</p> + * <p>This tag lists the valid modes for {@link CaptureRequest#NOISE_REDUCTION_MODE android.noiseReduction.mode}.</p> + * <p>Full-capability camera devices must laways support OFF and FAST.</p> + * + * @see CaptureRequest#NOISE_REDUCTION_MODE + */ + public static final Key<byte[]> NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES = + new Key<byte[]>("android.noiseReduction.availableNoiseReductionModes", byte[].class); + + /** + * <p>If set to 1, the HAL will always split result * metadata for a single capture into multiple buffers, - * returned using multiple process_capture_result calls. - * </p> - * <p> - * Does not need to be listed in static + * returned using multiple process_capture_result calls.</p> + * <p>Does not need to be listed in static * metadata. Support for partial results will be reworked in * future versions of camera service. This quirk will stop * working at that point; DO NOT USE without careful - * consideration of future support. - * </p> - * - * <b>Optional</b> - This value may be null on some devices. - * + * consideration of future support.</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> * @hide */ public static final Key<Byte> QUIRKS_USE_PARTIAL_RESULT = new Key<Byte>("android.quirks.usePartialResult", byte.class); /** - * <p> - * How many output streams can be allocated at - * the same time for each type of stream - * </p> - * <p> - * Video snapshot with preview callbacks requires 3 - * processed streams (preview, record, app callbacks) and - * one JPEG stream (snapshot) - * </p> + * <p>The maximum numbers of different types of output streams + * that can be configured and used simultaneously by a camera device.</p> + * <p>This is a 3 element tuple that contains the max number of output simultaneous + * streams for raw sensor, processed (and uncompressed), and JPEG formats respectively. + * For example, if max raw sensor format output stream number is 1, max YUV streams + * number is 3, and max JPEG stream number is 2, then this tuple should be <code>(1, 3, 2)</code>.</p> + * <p>This lists the upper bound of the number of output streams supported by + * the camera device. Using more streams simultaneously may require more hardware and + * CPU resources that will consume more power. The image format for a output stream can + * be any supported format provided by {@link CameraCharacteristics#SCALER_AVAILABLE_FORMATS android.scaler.availableFormats}. The formats + * defined in {@link CameraCharacteristics#SCALER_AVAILABLE_FORMATS android.scaler.availableFormats} can be catergorized into the 3 stream types + * as below:</p> + * <ul> + * <li>JPEG-compressed format: BLOB.</li> + * <li>Raw formats: RAW_SENSOR and RAW_OPAQUE.</li> + * <li>processed, uncompressed formats: YCbCr_420_888, YCrCb_420_SP, YV12.</li> + * </ul> + * + * @see CameraCharacteristics#SCALER_AVAILABLE_FORMATS */ public static final Key<int[]> REQUEST_MAX_NUM_OUTPUT_STREAMS = new Key<int[]>("android.request.maxNumOutputStreams", int[].class); /** - * <p> - * List of app-visible formats - * </p> + * <p>The maximum numbers of any type of input streams + * that can be configured and used simultaneously by a camera device.</p> + * <p>When set to 0, it means no input stream is supported.</p> + * <p>The image format for a input stream can be any supported + * format provided by + * {@link CameraCharacteristics#SCALER_AVAILABLE_INPUT_OUTPUT_FORMATS_MAP android.scaler.availableInputOutputFormatsMap}. When using an + * input stream, there must be at least one output stream + * configured to to receive the reprocessed images.</p> + * <p>For example, for Zero Shutter Lag (ZSL) still capture use case, the input + * stream image format will be RAW_OPAQUE, the associated output stream image format + * should be JPEG.</p> + * + * @see CameraCharacteristics#SCALER_AVAILABLE_INPUT_OUTPUT_FORMATS_MAP + */ + public static final Key<Integer> REQUEST_MAX_NUM_INPUT_STREAMS = + new Key<Integer>("android.request.maxNumInputStreams", int.class); + + /** + * <p>Specifies the number of maximum pipeline stages a frame + * has to go through from when it's exposed to when it's available + * to the framework.</p> + * <p>A typical minimum value for this is 2 (one stage to expose, + * one stage to readout) from the sensor. The ISP then usually adds + * its own stages to do custom HW processing. Further stages may be + * added by SW processing.</p> + * <p>Depending on what settings are used (e.g. YUV, JPEG) and what + * processing is enabled (e.g. face detection), the actual pipeline + * depth (specified by {@link CaptureResult#REQUEST_PIPELINE_DEPTH android.request.pipelineDepth}) may be less than + * the max pipeline depth.</p> + * <p>A pipeline depth of X stages is equivalent to a pipeline latency of + * X frame intervals.</p> + * <p>This value will be 8 or less.</p> + * + * @see CaptureResult#REQUEST_PIPELINE_DEPTH + */ + public static final Key<Byte> REQUEST_PIPELINE_MAX_DEPTH = + new Key<Byte>("android.request.pipelineMaxDepth", byte.class); + + /** + * <p>Optional. Defaults to 1. Defines how many sub-components + * a result will be composed of.</p> + * <p>In order to combat the pipeline latency, partial results + * may be delivered to the application layer from the camera device as + * soon as they are available.</p> + * <p>A value of 1 means that partial results are not supported.</p> + * <p>A typical use case for this might be: after requesting an AF lock the + * new AF state might be available 50% of the way through the pipeline. + * The camera device could then immediately dispatch this state via a + * partial result to the framework/application layer, and the rest of + * the metadata via later partial results.</p> + */ + public static final Key<Integer> REQUEST_PARTIAL_RESULT_COUNT = + new Key<Integer>("android.request.partialResultCount", int.class); + + /** + * <p>List of capabilities that the camera device + * advertises as fully supporting.</p> + * <p>A capability is a contract that the camera device makes in order + * to be able to satisfy one or more use cases.</p> + * <p>Listing a capability guarantees that the whole set of features + * required to support a common use will all be available.</p> + * <p>Using a subset of the functionality provided by an unsupported + * capability may be possible on a specific camera device implementation; + * to do this query each of android.request.availableRequestKeys, + * android.request.availableResultKeys, + * android.request.availableCharacteristicsKeys.</p> + * <p>XX: Maybe these should go into {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} + * as a table instead?</p> + * <p>The following capabilities are guaranteed to be available on + * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} <code>==</code> FULL devices:</p> + * <ul> + * <li>MANUAL_SENSOR</li> + * <li>ZSL</li> + * </ul> + * <p>Other capabilities may be available on either FULL or LIMITED + * devices, but the app. should query this field to be sure.</p> + * + * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL + * @see #REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE + * @see #REQUEST_AVAILABLE_CAPABILITIES_OPTIONAL + * @see #REQUEST_AVAILABLE_CAPABILITIES_MANUAL_SENSOR + * @see #REQUEST_AVAILABLE_CAPABILITIES_GCAM + * @see #REQUEST_AVAILABLE_CAPABILITIES_ZSL + * @see #REQUEST_AVAILABLE_CAPABILITIES_DNG + */ + public static final Key<Integer> REQUEST_AVAILABLE_CAPABILITIES = + new Key<Integer>("android.request.availableCapabilities", int.class); + + /** + * <p>A list of all keys that the camera device has available + * to use with CaptureRequest.</p> + * <p>Attempting to set a key into a CaptureRequest that is not + * listed here will result in an invalid request and will be rejected + * by the camera device.</p> + * <p>This field can be used to query the feature set of a camera device + * at a more granular level than capabilities. This is especially + * important for optional keys that are not listed under any capability + * in {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities}.</p> + * <p>TODO: This should be used by #getAvailableCaptureRequestKeys.</p> + * + * @see CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES + * @hide + */ + public static final Key<int[]> REQUEST_AVAILABLE_REQUEST_KEYS = + new Key<int[]>("android.request.availableRequestKeys", int[].class); + + /** + * <p>A list of all keys that the camera device has available + * to use with CaptureResult.</p> + * <p>Attempting to get a key from a CaptureResult that is not + * listed here will always return a <code>null</code> value. Getting a key from + * a CaptureResult that is listed here must never return a <code>null</code> + * value.</p> + * <p>The following keys may return <code>null</code> unless they are enabled:</p> + * <ul> + * <li>{@link CaptureResult#STATISTICS_LENS_SHADING_MAP android.statistics.lensShadingMap} (non-null iff {@link CaptureRequest#STATISTICS_LENS_SHADING_MAP_MODE android.statistics.lensShadingMapMode} == ON)</li> + * </ul> + * <p>(Those sometimes-null keys should nevertheless be listed here + * if they are available.)</p> + * <p>This field can be used to query the feature set of a camera device + * at a more granular level than capabilities. This is especially + * important for optional keys that are not listed under any capability + * in {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities}.</p> + * <p>TODO: This should be used by #getAvailableCaptureResultKeys.</p> + * + * @see CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES + * @see CaptureResult#STATISTICS_LENS_SHADING_MAP + * @see CaptureRequest#STATISTICS_LENS_SHADING_MAP_MODE + * @hide + */ + public static final Key<int[]> REQUEST_AVAILABLE_RESULT_KEYS = + new Key<int[]>("android.request.availableResultKeys", int[].class); + + /** + * <p>A list of all keys that the camera device has available + * to use with CameraCharacteristics.</p> + * <p>This entry follows the same rules as + * android.request.availableResultKeys (except that it applies for + * CameraCharacteristics instead of CaptureResult). See above for more + * details.</p> + * <p>TODO: This should be used by CameraCharacteristics#getKeys.</p> + * @hide + */ + public static final Key<int[]> REQUEST_AVAILABLE_CHARACTERISTICS_KEYS = + new Key<int[]>("android.request.availableCharacteristicsKeys", int[].class); + + /** + * <p>The list of image formats that are supported by this + * camera device for output streams.</p> + * <p>All camera devices will support JPEG and YUV_420_888 formats.</p> + * <p>When set to YUV_420_888, application can access the YUV420 data directly.</p> */ public static final Key<int[]> SCALER_AVAILABLE_FORMATS = new Key<int[]>("android.scaler.availableFormats", int[].class); /** - * <p> - * The minimum frame duration that is supported - * for each resolution in availableJpegSizes. Should - * correspond to the frame duration when only that JPEG - * stream is active and captured in a burst, with all - * processing set to FAST - * </p> - * <p> - * When multiple streams are configured, the minimum - * frame duration will be >= max(individual stream min - * durations) - * </p> + * <p>The minimum frame duration that is supported + * for each resolution in {@link CameraCharacteristics#SCALER_AVAILABLE_JPEG_SIZES android.scaler.availableJpegSizes}.</p> + * <p>This corresponds to the minimum steady-state frame duration when only + * that JPEG stream is active and captured in a burst, with all + * processing (typically in android.*.mode) set to FAST.</p> + * <p>When multiple streams are configured, the minimum + * frame duration will be >= max(individual stream min + * durations)</p> + * + * @see CameraCharacteristics#SCALER_AVAILABLE_JPEG_SIZES */ public static final Key<long[]> SCALER_AVAILABLE_JPEG_MIN_DURATIONS = new Key<long[]>("android.scaler.availableJpegMinDurations", long[].class); /** - * <p> - * The resolutions available for output from - * the JPEG block. Listed as width x height - * </p> + * <p>The JPEG resolutions that are supported by this camera device.</p> + * <p>The resolutions are listed as <code>(width, height)</code> pairs. All camera devices will support + * sensor maximum resolution (defined by {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE android.sensor.info.activeArraySize}).</p> + * + * @see CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE */ public static final Key<android.hardware.camera2.Size[]> SCALER_AVAILABLE_JPEG_SIZES = new Key<android.hardware.camera2.Size[]>("android.scaler.availableJpegSizes", android.hardware.camera2.Size[].class); /** - * <p> - * The maximum ratio between active area width + * <p>The maximum ratio between active area width * and crop region width, or between active area height and * crop region height, if the crop region height is larger - * than width - * </p> + * than width</p> */ public static final Key<Float> SCALER_AVAILABLE_MAX_DIGITAL_ZOOM = new Key<Float>("android.scaler.availableMaxDigitalZoom", float.class); /** - * <p> - * The minimum frame duration that is supported - * for each resolution in availableProcessedSizes. Should - * correspond to the frame duration when only that processed - * stream is active, with all processing set to - * FAST - * </p> - * <p> - * When multiple streams are configured, the minimum - * frame duration will be >= max(individual stream min - * durations) - * </p> + * <p>For each available processed output size (defined in + * {@link CameraCharacteristics#SCALER_AVAILABLE_PROCESSED_SIZES android.scaler.availableProcessedSizes}), this property lists the + * minimum supportable frame duration for that size.</p> + * <p>This should correspond to the frame duration when only that processed + * stream is active, with all processing (typically in android.*.mode) + * set to FAST.</p> + * <p>When multiple streams are configured, the minimum frame duration will + * be >= max(individual stream min durations).</p> + * + * @see CameraCharacteristics#SCALER_AVAILABLE_PROCESSED_SIZES */ public static final Key<long[]> SCALER_AVAILABLE_PROCESSED_MIN_DURATIONS = new Key<long[]>("android.scaler.availableProcessedMinDurations", long[].class); /** - * <p> - * The resolutions available for use with + * <p>The resolutions available for use with * processed output streams, such as YV12, NV12, and * platform opaque YUV/RGB streams to the GPU or video - * encoders. Listed as width, height - * </p> - * <p> - * The actual supported resolution list may be limited by - * consumer end points for different use cases. For example, for - * recording use case, the largest supported resolution may be - * limited by max supported size from encoder, for preview use - * case, the largest supported resolution may be limited by max - * resolution SurfaceTexture/SurfaceView can support. - * </p> + * encoders.</p> + * <p>The resolutions are listed as <code>(width, height)</code> pairs.</p> + * <p>For a given use case, the actual maximum supported resolution + * may be lower than what is listed here, depending on the destination + * Surface for the image data. For example, for recording video, + * the video encoder chosen may have a maximum size limit (e.g. 1080p) + * smaller than what the camera (e.g. maximum resolution is 3264x2448) + * can provide.</p> + * <p>Please reference the documentation for the image data destination to + * check if it limits the maximum size for image data.</p> */ public static final Key<android.hardware.camera2.Size[]> SCALER_AVAILABLE_PROCESSED_SIZES = new Key<android.hardware.camera2.Size[]>("android.scaler.availableProcessedSizes", android.hardware.camera2.Size[].class); /** - * <p> - * Area of raw data which corresponds to only - * active pixels; smaller or equal to - * pixelArraySize. - * </p> + * <p>The mapping of image formats that are supported by this + * camera device for input streams, to their corresponding output formats.</p> + * <p>All camera devices with at least 1 + * {@link CameraCharacteristics#REQUEST_MAX_NUM_INPUT_STREAMS android.request.maxNumInputStreams} will have at least one + * available input format.</p> + * <p>The camera device will support the following map of formats, + * if its dependent capability is supported:</p> + * <table> + * <thead> + * <tr> + * <th align="left">Input Format</th> + * <th align="left">Output Format</th> + * <th align="left">Capability</th> + * </tr> + * </thead> + * <tbody> + * <tr> + * <td align="left">RAW_OPAQUE</td> + * <td align="left">JPEG</td> + * <td align="left">ZSL</td> + * </tr> + * <tr> + * <td align="left">RAW_OPAQUE</td> + * <td align="left">YUV_420_888</td> + * <td align="left">ZSL</td> + * </tr> + * <tr> + * <td align="left">RAW_OPAQUE</td> + * <td align="left">RAW16</td> + * <td align="left">DNG</td> + * </tr> + * <tr> + * <td align="left">RAW16</td> + * <td align="left">YUV_420_888</td> + * <td align="left">DNG</td> + * </tr> + * <tr> + * <td align="left">RAW16</td> + * <td align="left">JPEG</td> + * <td align="left">DNG</td> + * </tr> + * </tbody> + * </table> + * <p>For ZSL-capable camera devices, using the RAW_OPAQUE format + * as either input or output will never hurt maximum frame rate (i.e. + * {@link CameraCharacteristics#SCALER_AVAILABLE_STALL_DURATIONS android.scaler.availableStallDurations} will not have RAW_OPAQUE).</p> + * <p>Attempting to configure an input stream with output streams not + * listed as available in this map is not valid.</p> + * <p>TODO: Add java type mapping for this property.</p> + * + * @see CameraCharacteristics#REQUEST_MAX_NUM_INPUT_STREAMS + * @see CameraCharacteristics#SCALER_AVAILABLE_STALL_DURATIONS + */ + public static final Key<int[]> SCALER_AVAILABLE_INPUT_OUTPUT_FORMATS_MAP = + new Key<int[]>("android.scaler.availableInputOutputFormatsMap", int[].class); + + /** + * <p>The available stream configurations that this + * camera device supports + * (i.e. format, width, height, output/input stream).</p> + * <p>The configurations are listed as <code>(format, width, height, input?)</code> + * tuples.</p> + * <p>All camera devices will support sensor maximum resolution (defined by + * {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE android.sensor.info.activeArraySize}) for the JPEG format.</p> + * <p>For a given use case, the actual maximum supported resolution + * may be lower than what is listed here, depending on the destination + * Surface for the image data. For example, for recording video, + * the video encoder chosen may have a maximum size limit (e.g. 1080p) + * smaller than what the camera (e.g. maximum resolution is 3264x2448) + * can provide.</p> + * <p>Please reference the documentation for the image data destination to + * check if it limits the maximum size for image data.</p> + * <p>Not all output formats may be supported in a configuration with + * an input stream of a particular format. For more details, see + * {@link CameraCharacteristics#SCALER_AVAILABLE_INPUT_OUTPUT_FORMATS_MAP android.scaler.availableInputOutputFormatsMap}.</p> + * <p>The following table describes the minimum required output stream + * configurations based on the hardware level + * ({@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel}):</p> + * <table> + * <thead> + * <tr> + * <th align="center">Format</th> + * <th align="center">Size</th> + * <th align="center">Hardware Level</th> + * <th align="center">Notes</th> + * </tr> + * </thead> + * <tbody> + * <tr> + * <td align="center">JPEG</td> + * <td align="center">{@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE android.sensor.info.activeArraySize}</td> + * <td align="center">Any</td> + * <td align="center"></td> + * </tr> + * <tr> + * <td align="center">JPEG</td> + * <td align="center">1920x1080 (1080p)</td> + * <td align="center">Any</td> + * <td align="center">if 1080p <= activeArraySize</td> + * </tr> + * <tr> + * <td align="center">JPEG</td> + * <td align="center">1280x720 (720)</td> + * <td align="center">Any</td> + * <td align="center">if 720p <= activeArraySize</td> + * </tr> + * <tr> + * <td align="center">JPEG</td> + * <td align="center">640x480 (480p)</td> + * <td align="center">Any</td> + * <td align="center">if 480p <= activeArraySize</td> + * </tr> + * <tr> + * <td align="center">JPEG</td> + * <td align="center">320x240 (240p)</td> + * <td align="center">Any</td> + * <td align="center">if 240p <= activeArraySize</td> + * </tr> + * <tr> + * <td align="center">YUV_420_888</td> + * <td align="center">all output sizes available for JPEG</td> + * <td align="center">FULL</td> + * <td align="center"></td> + * </tr> + * <tr> + * <td align="center">YUV_420_888</td> + * <td align="center">all output sizes available for JPEG, up to the maximum video size</td> + * <td align="center">LIMITED</td> + * <td align="center"></td> + * </tr> + * <tr> + * <td align="center">IMPLEMENTATION_DEFINED</td> + * <td align="center">same as YUV_420_888</td> + * <td align="center">Any</td> + * <td align="center"></td> + * </tr> + * </tbody> + * </table> + * <p>Refer to {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} for additional + * mandatory stream configurations on a per-capability basis.</p> + * + * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL + * @see CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES + * @see CameraCharacteristics#SCALER_AVAILABLE_INPUT_OUTPUT_FORMATS_MAP + * @see CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE + * @see #SCALER_AVAILABLE_STREAM_CONFIGURATIONS_OUTPUT + * @see #SCALER_AVAILABLE_STREAM_CONFIGURATIONS_INPUT + */ + public static final Key<int[]> SCALER_AVAILABLE_STREAM_CONFIGURATIONS = + new Key<int[]>("android.scaler.availableStreamConfigurations", int[].class); + + /** + * <p>This lists the minimum frame duration for each + * format/size combination.</p> + * <p>This should correspond to the frame duration when only that + * stream is active, with all processing (typically in android.*.mode) + * set to either OFF or FAST.</p> + * <p>When multiple streams are used in a request, the minimum frame + * duration will be max(individual stream min durations).</p> + * <p>The minimum frame duration of a stream (of a particular format, size) + * is the same regardless of whether the stream is input or output.</p> + * <p>See {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration} and + * {@link CameraCharacteristics#SCALER_AVAILABLE_STALL_DURATIONS android.scaler.availableStallDurations} for more details about + * calculating the max frame rate.</p> + * + * @see CameraCharacteristics#SCALER_AVAILABLE_STALL_DURATIONS + * @see CaptureRequest#SENSOR_FRAME_DURATION + */ + public static final Key<long[]> SCALER_AVAILABLE_MIN_FRAME_DURATIONS = + new Key<long[]>("android.scaler.availableMinFrameDurations", long[].class); + + /** + * <p>This lists the maximum stall duration for each + * format/size combination.</p> + * <p>A stall duration is how much extra time would get added + * to the normal minimum frame duration for a repeating request + * that has streams with non-zero stall.</p> + * <p>For example, consider JPEG captures which have the following + * characteristics:</p> + * <ul> + * <li>JPEG streams act like processed YUV streams in requests for which + * they are not included; in requests in which they are directly + * referenced, they act as JPEG streams. This is because supporting a + * JPEG stream requires the underlying YUV data to always be ready for + * use by a JPEG encoder, but the encoder will only be used (and impact + * frame duration) on requests that actually reference a JPEG stream.</li> + * <li>The JPEG processor can run concurrently to the rest of the camera + * pipeline, but cannot process more than 1 capture at a time.</li> + * </ul> + * <p>In other words, using a repeating YUV request would result + * in a steady frame rate (let's say it's 30 FPS). If a single + * JPEG request is submitted periodically, the frame rate will stay + * at 30 FPS (as long as we wait for the previous JPEG to return each + * time). If we try to submit a repeating YUV + JPEG request, then + * the frame rate will drop from 30 FPS.</p> + * <p>In general, submitting a new request with a non-0 stall time + * stream will <em>not</em> cause a frame rate drop unless there are still + * outstanding buffers for that stream from previous requests.</p> + * <p>Submitting a repeating request with streams (call this <code>S</code>) + * is the same as setting the minimum frame duration from + * the normal minimum frame duration corresponding to <code>S</code>, added with + * the maximum stall duration for <code>S</code>.</p> + * <p>If interleaving requests with and without a stall duration, + * a request will stall by the maximum of the remaining times + * for each can-stall stream with outstanding buffers.</p> + * <p>This means that a stalling request will not have an exposure start + * until the stall has completed.</p> + * <p>This should correspond to the stall duration when only that stream is + * active, with all processing (typically in android.*.mode) set to FAST + * or OFF. Setting any of the processing modes to HIGH_QUALITY + * effectively results in an indeterminate stall duration for all + * streams in a request (the regular stall calculation rules are + * ignored).</p> + * <p>The following formats may always have a stall duration:</p> + * <ul> + * <li>JPEG</li> + * <li>RAW16</li> + * </ul> + * <p>The following formats will never have a stall duration:</p> + * <ul> + * <li>YUV_420_888</li> + * <li>IMPLEMENTATION_DEFINED</li> + * </ul> + * <p>All other formats may or may not have an allowed stall duration on + * a per-capability basis; refer to {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} + * for more details.</p> + * <p>See {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration} for more information about + * calculating the max frame rate (absent stalls).</p> + * + * @see CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES + * @see CaptureRequest#SENSOR_FRAME_DURATION + */ + public static final Key<long[]> SCALER_AVAILABLE_STALL_DURATIONS = + new Key<long[]>("android.scaler.availableStallDurations", long[].class); + + /** + * <p>Area of raw data which corresponds to only + * active pixels.</p> + * <p>It is smaller or equal to + * sensor full pixel array, which could include the black calibration pixels.</p> */ public static final Key<android.graphics.Rect> SENSOR_INFO_ACTIVE_ARRAY_SIZE = new Key<android.graphics.Rect>("android.sensor.info.activeArraySize", android.graphics.Rect.class); /** - * <p> - * Range of valid sensitivities - * </p> + * <p>Range of valid sensitivities</p> */ public static final Key<int[]> SENSOR_INFO_SENSITIVITY_RANGE = new Key<int[]>("android.sensor.info.sensitivityRange", int[].class); /** - * <p> - * Range of valid exposure - * times - * </p> + * <p>Arrangement of color filters on sensor; + * represents the colors in the top-left 2x2 section of + * the sensor, in reading order</p> + * @see #SENSOR_INFO_COLOR_FILTER_ARRANGEMENT_RGGB + * @see #SENSOR_INFO_COLOR_FILTER_ARRANGEMENT_GRBG + * @see #SENSOR_INFO_COLOR_FILTER_ARRANGEMENT_GBRG + * @see #SENSOR_INFO_COLOR_FILTER_ARRANGEMENT_BGGR + * @see #SENSOR_INFO_COLOR_FILTER_ARRANGEMENT_RGB + */ + public static final Key<Integer> SENSOR_INFO_COLOR_FILTER_ARRANGEMENT = + new Key<Integer>("android.sensor.info.colorFilterArrangement", int.class); + + /** + * <p>Range of valid exposure + * times used by {@link CaptureRequest#SENSOR_EXPOSURE_TIME android.sensor.exposureTime}.</p> + * + * @see CaptureRequest#SENSOR_EXPOSURE_TIME */ public static final Key<long[]> SENSOR_INFO_EXPOSURE_TIME_RANGE = new Key<long[]>("android.sensor.info.exposureTimeRange", long[].class); /** - * <p> - * Maximum possible frame duration (minimum frame - * rate) - * </p> - * <p> - * Minimum duration is a function of resolution, - * processing settings. See - * android.scaler.availableProcessedMinDurations - * android.scaler.availableJpegMinDurations - * android.scaler.availableRawMinDurations - * </p> + * <p>Maximum possible frame duration (minimum frame + * rate).</p> + * <p>The largest possible {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration} + * that will be accepted by the camera device. Attempting to use + * frame durations beyond the maximum will result in the frame duration + * being clipped to the maximum. See that control + * for a full definition of frame durations.</p> + * <p>Refer to + * {@link CameraCharacteristics#SCALER_AVAILABLE_PROCESSED_MIN_DURATIONS android.scaler.availableProcessedMinDurations}, + * {@link CameraCharacteristics#SCALER_AVAILABLE_JPEG_MIN_DURATIONS android.scaler.availableJpegMinDurations}, and + * android.scaler.availableRawMinDurations for the minimum + * frame duration values.</p> + * + * @see CameraCharacteristics#SCALER_AVAILABLE_JPEG_MIN_DURATIONS + * @see CameraCharacteristics#SCALER_AVAILABLE_PROCESSED_MIN_DURATIONS + * @see CaptureRequest#SENSOR_FRAME_DURATION */ public static final Key<Long> SENSOR_INFO_MAX_FRAME_DURATION = new Key<Long>("android.sensor.info.maxFrameDuration", long.class); /** - * <p> - * The physical dimensions of the full pixel - * array - * </p> - * <p> - * Needed for FOV calculation for old API - * </p> + * <p>The physical dimensions of the full pixel + * array</p> + * <p>Needed for FOV calculation for old API</p> */ public static final Key<float[]> SENSOR_INFO_PHYSICAL_SIZE = new Key<float[]>("android.sensor.info.physicalSize", float[].class); /** - * <p> - * Gain factor from electrons to raw units when - * ISO=100 - * </p> + * <p>Dimensions of full pixel array, possibly + * including black calibration pixels.</p> + * <p>Maximum output resolution for raw format must + * match this in + * {@link CameraCharacteristics#SCALER_AVAILABLE_STREAM_CONFIGURATIONS android.scaler.availableStreamConfigurations}.</p> * - * <b>Optional</b> - This value may be null on some devices. + * @see CameraCharacteristics#SCALER_AVAILABLE_STREAM_CONFIGURATIONS + */ + public static final Key<android.hardware.camera2.Size> SENSOR_INFO_PIXEL_ARRAY_SIZE = + new Key<android.hardware.camera2.Size>("android.sensor.info.pixelArraySize", android.hardware.camera2.Size.class); + + /** + * <p>Maximum raw value output by sensor.</p> + * <p>This specifies the fully-saturated encoding level for the raw + * sample values from the sensor. This is typically caused by the + * sensor becoming highly non-linear or clipping. The minimum for + * each channel is specified by the offset in the + * {@link CameraCharacteristics#SENSOR_BLACK_LEVEL_PATTERN android.sensor.blackLevelPattern} tag.</p> + * <p>The white level is typically determined either by sensor bit depth + * (8-14 bits is expected), or by the point where the sensor response + * becomes too non-linear to be useful. The default value for this is + * maximum representable value for a 16-bit raw sample (2^16 - 1).</p> + * + * @see CameraCharacteristics#SENSOR_BLACK_LEVEL_PATTERN + */ + public static final Key<Integer> SENSOR_INFO_WHITE_LEVEL = + new Key<Integer>("android.sensor.info.whiteLevel", int.class); + + /** + * <p>Gain factor from electrons to raw units when + * ISO=100</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + * <p><b>Full capability</b> - + * Present on all camera devices that report being {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_FULL HARDWARE_LEVEL_FULL} devices in the + * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p> * - * <b>{@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_FULL HARDWARE_LEVEL_FULL}</b> - - * Present on all devices that report being FULL level hardware devices in the - * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL HARDWARE_LEVEL} key. + * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL */ public static final Key<Rational> SENSOR_BASE_GAIN_FACTOR = new Key<Rational>("android.sensor.baseGainFactor", Rational.class); /** - * <p> - * Maximum sensitivity that is implemented - * purely through analog gain - * </p> - * <p> - * For android.sensor.sensitivity values less than or - * equal to this, all applied gain must be analog. For - * values above this, it can be a mix of analog and - * digital - * </p> + * <p>A fixed black level offset for each of the color filter arrangement + * (CFA) mosaic channels.</p> + * <p>This tag specifies the zero light value for each of the CFA mosaic + * channels in the camera sensor. The maximal value output by the + * sensor is represented by the value in {@link CameraCharacteristics#SENSOR_INFO_WHITE_LEVEL android.sensor.info.whiteLevel}.</p> + * <p>The values are given in row-column scan order, with the first value + * corresponding to the element of the CFA in row=0, column=0.</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> * - * <b>Optional</b> - This value may be null on some devices. + * @see CameraCharacteristics#SENSOR_INFO_WHITE_LEVEL + */ + public static final Key<int[]> SENSOR_BLACK_LEVEL_PATTERN = + new Key<int[]>("android.sensor.blackLevelPattern", int[].class); + + /** + * <p>Maximum sensitivity that is implemented + * purely through analog gain.</p> + * <p>For {@link CaptureRequest#SENSOR_SENSITIVITY android.sensor.sensitivity} values less than or + * equal to this, all applied gain must be analog. For + * values above this, the gain applied can be a mix of analog and + * digital.</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + * <p><b>Full capability</b> - + * Present on all camera devices that report being {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_FULL HARDWARE_LEVEL_FULL} devices in the + * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p> * - * <b>{@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_FULL HARDWARE_LEVEL_FULL}</b> - - * Present on all devices that report being FULL level hardware devices in the - * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL HARDWARE_LEVEL} key. + * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL + * @see CaptureRequest#SENSOR_SENSITIVITY */ public static final Key<Integer> SENSOR_MAX_ANALOG_SENSITIVITY = new Key<Integer>("android.sensor.maxAnalogSensitivity", int.class); /** - * <p> - * Clockwise angle through which the output + * <p>Clockwise angle through which the output * image needs to be rotated to be upright on the device * screen in its native orientation. Also defines the * direction of rolling shutter readout, which is from top - * to bottom in the sensor's coordinate system - * </p> + * to bottom in the sensor's coordinate system</p> */ public static final Key<Integer> SENSOR_ORIENTATION = new Key<Integer>("android.sensor.orientation", int.class); /** - * <p> - * Which face detection modes are available, - * if any - * </p> - * <p> - * OFF means face detection is disabled, it must - * be included in the list. - * </p><p> - * SIMPLE means the device supports the + * <p>The number of input samples for each dimension of + * {@link CaptureResult#SENSOR_PROFILE_HUE_SAT_MAP android.sensor.profileHueSatMap}.</p> + * <p>The number of input samples for the hue, saturation, and value + * dimension of {@link CaptureResult#SENSOR_PROFILE_HUE_SAT_MAP android.sensor.profileHueSatMap}. The order of the + * dimensions given is hue, saturation, value; where hue is the 0th + * element.</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + * + * @see CaptureResult#SENSOR_PROFILE_HUE_SAT_MAP + */ + public static final Key<int[]> SENSOR_PROFILE_HUE_SAT_MAP_DIMENSIONS = + new Key<int[]>("android.sensor.profileHueSatMapDimensions", int[].class); + + /** + * <p>Optional. Defaults to [OFF]. Lists the supported test + * pattern modes for {@link CaptureRequest#SENSOR_TEST_PATTERN_MODE android.sensor.testPatternMode}.</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + * + * @see CaptureRequest#SENSOR_TEST_PATTERN_MODE + */ + public static final Key<int[]> SENSOR_AVAILABLE_TEST_PATTERN_MODES = + new Key<int[]>("android.sensor.availableTestPatternModes", int[].class); + + /** + * <p>Which face detection modes are available, + * if any</p> + * <p>OFF means face detection is disabled, it must + * be included in the list.</p> + * <p>SIMPLE means the device supports the * android.statistics.faceRectangles and - * android.statistics.faceScores outputs. - * </p><p> - * FULL means the device additionally supports the + * android.statistics.faceScores outputs.</p> + * <p>FULL means the device additionally supports the * android.statistics.faceIds and - * android.statistics.faceLandmarks outputs. - * </p> + * android.statistics.faceLandmarks outputs.</p> */ public static final Key<byte[]> STATISTICS_INFO_AVAILABLE_FACE_DETECT_MODES = new Key<byte[]>("android.statistics.info.availableFaceDetectModes", byte[].class); /** - * <p> - * Maximum number of simultaneously detectable - * faces - * </p> + * <p>Maximum number of simultaneously detectable + * faces</p> */ public static final Key<Integer> STATISTICS_INFO_MAX_FACE_COUNT = new Key<Integer>("android.statistics.info.maxFaceCount", int.class); /** - * <p> - * Maximum number of supported points in the - * tonemap curve - * </p> + * <p>The set of hot pixel map output modes supported by this camera device.</p> + * <p>This tag lists valid output modes for {@link CaptureRequest#STATISTICS_HOT_PIXEL_MAP_MODE android.statistics.hotPixelMapMode}.</p> + * <p>If no hotpixel map is available for this camera device, this will contain + * only OFF. If the hotpixel map is available, this should include both + * the ON and OFF options.</p> + * + * @see CaptureRequest#STATISTICS_HOT_PIXEL_MAP_MODE + */ + public static final Key<boolean[]> STATISTICS_INFO_AVAILABLE_HOT_PIXEL_MAP_MODES = + new Key<boolean[]>("android.statistics.info.availableHotPixelMapModes", boolean[].class); + + /** + * <p>Maximum number of supported points in the + * tonemap curve that can be used for {@link CaptureRequest#TONEMAP_CURVE_RED android.tonemap.curveRed}, or + * {@link CaptureRequest#TONEMAP_CURVE_GREEN android.tonemap.curveGreen}, or {@link CaptureRequest#TONEMAP_CURVE_BLUE android.tonemap.curveBlue}.</p> + * <p>If the actual number of points provided by the application (in + * android.tonemap.curve*) is less than max, the camera device will + * resample the curve to its internal representation, using linear + * interpolation.</p> + * <p>The output curves in the result metadata may have a different number + * of points than the input curves, and will represent the actual + * hardware curves used as closely as possible when linearly interpolated.</p> + * + * @see CaptureRequest#TONEMAP_CURVE_BLUE + * @see CaptureRequest#TONEMAP_CURVE_GREEN + * @see CaptureRequest#TONEMAP_CURVE_RED */ public static final Key<Integer> TONEMAP_MAX_CURVE_POINTS = new Key<Integer>("android.tonemap.maxCurvePoints", int.class); /** - * <p> - * A list of camera LEDs that are available on this system. - * </p> - * @see #LED_AVAILABLE_LEDS_TRANSMIT + * <p>The set of tonemapping modes supported by this camera device.</p> + * <p>This tag lists the valid modes for {@link CaptureRequest#TONEMAP_MODE android.tonemap.mode}.</p> + * <p>Full-capability camera devices must always support CONTRAST_CURVE and + * FAST.</p> * + * @see CaptureRequest#TONEMAP_MODE + */ + public static final Key<byte[]> TONEMAP_AVAILABLE_TONE_MAP_MODES = + new Key<byte[]>("android.tonemap.availableToneMapModes", byte[].class); + + /** + * <p>A list of camera LEDs that are available on this system.</p> + * @see #LED_AVAILABLE_LEDS_TRANSMIT * @hide */ public static final Key<int[]> LED_AVAILABLE_LEDS = new Key<int[]>("android.led.availableLeds", int[].class); /** - * <p> - * The camera 3 HAL device can implement one of two possible - * operational modes; limited and full. Full support is - * expected from new higher-end devices. Limited mode has - * hardware requirements roughly in line with those for a - * camera HAL device v1 implementation, and is expected from - * older or inexpensive devices. Full is a strict superset of - * limited, and they share the same essential operational flow. - * </p><p> - * For full details refer to "S3. Operational Modes" in camera3.h - * </p> + * <p>Generally classifies the overall set of the camera device functionality.</p> + * <p>Camera devices will come in two flavors: LIMITED and FULL.</p> + * <p>A FULL device has the most support possible and will enable the + * widest range of use cases such as:</p> + * <ul> + * <li>30 FPS at maximum resolution (== sensor resolution)</li> + * <li>Per frame control</li> + * <li>Manual sensor control</li> + * <li>Zero Shutter Lag (ZSL)</li> + * </ul> + * <p>A LIMITED device may have some or none of the above characteristics. + * To find out more refer to {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities}.</p> + * + * @see CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES * @see #INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED * @see #INFO_SUPPORTED_HARDWARE_LEVEL_FULL */ public static final Key<Integer> INFO_SUPPORTED_HARDWARE_LEVEL = new Key<Integer>("android.info.supportedHardwareLevel", int.class); + /** + * <p>The maximum number of frames that can occur after a request + * (different than the previous) has been submitted, and before the + * result's state becomes synchronized (by setting + * android.sync.frameNumber to a non-negative value).</p> + * <p>This defines the maximum distance (in number of metadata results), + * between android.sync.frameNumber and the equivalent + * android.request.frameCount.</p> + * <p>In other words this acts as an upper boundary for how many frames + * must occur before the camera device knows for a fact that the new + * submitted camera settings have been applied in outgoing frames.</p> + * <p>For example if the distance was 2,</p> + * <pre><code>initial request = X (repeating) + * request1 = X + * request2 = Y + * request3 = Y + * request4 = Y + * + * where requestN has frameNumber N, and the first of the repeating + * initial request's has frameNumber F (and F < 1). + * + * initial result = X' + { android.sync.frameNumber == F } + * result1 = X' + { android.sync.frameNumber == F } + * result2 = X' + { android.sync.frameNumber == CONVERGING } + * result3 = X' + { android.sync.frameNumber == CONVERGING } + * result4 = X' + { android.sync.frameNumber == 2 } + * + * where resultN has frameNumber N. + * </code></pre> + * <p>Since <code>result4</code> has a <code>frameNumber == 4</code> and + * <code>android.sync.frameNumber == 2</code>, the distance is clearly + * <code>4 - 2 = 2</code>.</p> + * @see #SYNC_MAX_LATENCY_PER_FRAME_CONTROL + * @see #SYNC_MAX_LATENCY_UNKNOWN + */ + public static final Key<Integer> SYNC_MAX_LATENCY = + new Key<Integer>("android.sync.maxLatency", int.class); + /*~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~ * End generated code *~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~O@*/ diff --git a/core/java/android/hardware/camera2/CameraDevice.java b/core/java/android/hardware/camera2/CameraDevice.java index 9e8d7d1..2c53f03 100644 --- a/core/java/android/hardware/camera2/CameraDevice.java +++ b/core/java/android/hardware/camera2/CameraDevice.java @@ -92,7 +92,6 @@ public interface CameraDevice extends AutoCloseable { * AE/AWB/AF should be on auto mode. * * @see #createCaptureRequest - * @hide */ public static final int TEMPLATE_ZERO_SHUTTER_LAG = 5; @@ -105,7 +104,6 @@ public interface CameraDevice extends AutoCloseable { * application depending on the intended use case. * * @see #createCaptureRequest - * @hide */ public static final int TEMPLATE_MANUAL = 6; @@ -501,31 +499,6 @@ public interface CameraDevice extends AutoCloseable { public void stopRepeating() throws CameraAccessException; /** - * <p>Wait until all the submitted requests have finished processing</p> - * - * <p>This method blocks until all the requests that have been submitted to - * the camera device, either through {@link #capture capture}, - * {@link #captureBurst captureBurst}, - * {@link #setRepeatingRequest setRepeatingRequest}, or - * {@link #setRepeatingBurst setRepeatingBurst}, have completed their - * processing.</p> - * - * <p>Once this call returns successfully, the device is in an idle state, - * and can be reconfigured with {@link #configureOutputs configureOutputs}.</p> - * - * <p>This method cannot be used if there is an active repeating request or - * burst, set with {@link #setRepeatingRequest setRepeatingRequest} or - * {@link #setRepeatingBurst setRepeatingBurst}. Call - * {@link #stopRepeating stopRepeating} before calling this method.</p> - * - * @throws CameraAccessException if the camera device is no longer connected - * @throws IllegalStateException if the camera device has been closed, the - * device has encountered a fatal error, or if there is an active repeating - * request or burst. - */ - public void waitUntilIdle() throws CameraAccessException; - - /** * Flush all captures currently pending and in-progress as fast as * possible. * diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java index 65b6c7a..0fcd598 100644 --- a/core/java/android/hardware/camera2/CameraManager.java +++ b/core/java/android/hardware/camera2/CameraManager.java @@ -19,7 +19,6 @@ package android.hardware.camera2; import android.content.Context; import android.hardware.ICameraService; import android.hardware.ICameraServiceListener; -import android.hardware.IProCameraUser; import android.hardware.camera2.impl.CameraMetadataNative; import android.hardware.camera2.utils.CameraBinderDecorator; import android.hardware.camera2.utils.CameraRuntimeException; @@ -49,6 +48,8 @@ import java.util.ArrayList; */ public final class CameraManager { + private static final String TAG = "CameraManager"; + /** * This should match the ICameraService definition */ @@ -80,6 +81,14 @@ public final class CameraManager { mCameraService = CameraBinderDecorator.newInstance(cameraServiceRaw); try { + CameraBinderDecorator.throwOnError( + CameraMetadataNative.nativeSetupGlobalVendorTagDescriptor()); + } catch(CameraRuntimeException e) { + throw new IllegalStateException("Failed to setup camera vendor tags", + e.asChecked()); + } + + try { mCameraService.addListener(new CameraServiceListener()); } catch(CameraRuntimeException e) { throw new IllegalStateException("Failed to register a camera service listener", diff --git a/core/java/android/hardware/camera2/CameraMetadata.java b/core/java/android/hardware/camera2/CameraMetadata.java index 1d6ff7d..9b1bc53 100644 --- a/core/java/android/hardware/camera2/CameraMetadata.java +++ b/core/java/android/hardware/camera2/CameraMetadata.java @@ -168,7 +168,7 @@ public abstract class CameraMetadata { Key lhs = (Key) o; - return mName.equals(lhs.mName); + return mName.equals(lhs.mName) && mType.equals(lhs.mType); } /** @@ -206,6 +206,45 @@ public abstract class CameraMetadata { *~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~*/ // + // Enumeration values for CameraCharacteristics#LENS_INFO_FOCUS_DISTANCE_CALIBRATION + // + + /** + * <p>The lens focus distance is not accurate, and the units used for + * {@link CaptureRequest#LENS_FOCUS_DISTANCE android.lens.focusDistance} do not correspond to any physical units. + * Setting the lens to the same focus distance on separate occasions may + * result in a different real focus distance, depending on factors such + * as the orientation of the device, the age of the focusing mechanism, + * and the device temperature. The focus distance value will still be + * in the range of <code>[0, {@link CameraCharacteristics#LENS_INFO_MINIMUM_FOCUS_DISTANCE android.lens.info.minimumFocusDistance}]</code>, where 0 + * represents the farthest focus.</p> + * + * @see CaptureRequest#LENS_FOCUS_DISTANCE + * @see CameraCharacteristics#LENS_INFO_MINIMUM_FOCUS_DISTANCE + * @see CameraCharacteristics#LENS_INFO_FOCUS_DISTANCE_CALIBRATION + */ + public static final int LENS_INFO_FOCUS_DISTANCE_CALIBRATION_UNCALIBRATED = 0; + + /** + * <p>The lens focus distance is measured in diopters. However, setting the lens + * to the same focus distance on separate occasions may result in a + * different real focus distance, depending on factors such as the + * orientation of the device, the age of the focusing mechanism, and + * the device temperature.</p> + * @see CameraCharacteristics#LENS_INFO_FOCUS_DISTANCE_CALIBRATION + */ + public static final int LENS_INFO_FOCUS_DISTANCE_CALIBRATION_APPROXIMATE = 1; + + /** + * <p>The lens focus distance is measured in diopters. The lens mechanism is + * calibrated so that setting the same focus distance is repeatable on + * multiple occasions with good accuracy, and the focus distance corresponds + * to the real physical distance to the plane of best focus.</p> + * @see CameraCharacteristics#LENS_INFO_FOCUS_DISTANCE_CALIBRATION + */ + public static final int LENS_INFO_FOCUS_DISTANCE_CALIBRATION_CALIBRATED = 2; + + // // Enumeration values for CameraCharacteristics#LENS_FACING // @@ -220,13 +259,209 @@ public abstract class CameraMetadata { public static final int LENS_FACING_BACK = 1; // + // Enumeration values for CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES + // + + /** + * <p>The minimal set of capabilities that every camera + * device (regardless of {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel}) + * will support.</p> + * <p>The full set of features supported by this capability makes + * the camera2 api backwards compatible with the camera1 + * (android.hardware.Camera) API.</p> + * <p>TODO: @hide this. Doesn't really mean anything except + * act as a catch-all for all the 'base' functionality.</p> + * + * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL + * @see CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES + */ + public static final int REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE = 0; + + /** + * <p>This is a catch-all capability to include all other + * tags or functionality not encapsulated by one of the other + * capabilities.</p> + * <p>A typical example is all tags marked 'optional'.</p> + * <p>TODO: @hide. We may not need this if we @hide all the optional + * tags not belonging to a capability.</p> + * @see CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES + */ + public static final int REQUEST_AVAILABLE_CAPABILITIES_OPTIONAL = 1; + + /** + * <p>The camera device can be manually controlled (3A algorithms such + * as auto exposure, and auto focus can be + * bypassed), this includes but is not limited to:</p> + * <ul> + * <li>Manual exposure control<ul> + * <li>{@link CaptureRequest#SENSOR_EXPOSURE_TIME android.sensor.exposureTime}</li> + * <li>{@link CameraCharacteristics#SENSOR_INFO_EXPOSURE_TIME_RANGE android.sensor.info.exposureTimeRange}</li> + * </ul> + * </li> + * <li>Manual sensitivity control<ul> + * <li>{@link CaptureRequest#SENSOR_SENSITIVITY android.sensor.sensitivity}</li> + * <li>{@link CameraCharacteristics#SENSOR_INFO_SENSITIVITY_RANGE android.sensor.info.sensitivityRange}</li> + * <li>{@link CameraCharacteristics#SENSOR_BASE_GAIN_FACTOR android.sensor.baseGainFactor}</li> + * </ul> + * </li> + * <li>Manual lens control<ul> + * <li>android.lens.*</li> + * </ul> + * </li> + * <li>Manual flash control<ul> + * <li>android.flash.*</li> + * </ul> + * </li> + * <li>Manual black level locking<ul> + * <li>{@link CaptureRequest#BLACK_LEVEL_LOCK android.blackLevel.lock}</li> + * </ul> + * </li> + * </ul> + * <p>If any of the above 3A algorithms are enabled, then the camera + * device will accurately report the values applied by 3A in the + * result.</p> + * + * @see CaptureRequest#BLACK_LEVEL_LOCK + * @see CameraCharacteristics#SENSOR_BASE_GAIN_FACTOR + * @see CaptureRequest#SENSOR_EXPOSURE_TIME + * @see CameraCharacteristics#SENSOR_INFO_EXPOSURE_TIME_RANGE + * @see CameraCharacteristics#SENSOR_INFO_SENSITIVITY_RANGE + * @see CaptureRequest#SENSOR_SENSITIVITY + * @see CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES + */ + public static final int REQUEST_AVAILABLE_CAPABILITIES_MANUAL_SENSOR = 2; + + /** + * <p>TODO: This should be @hide</p> + * <ul> + * <li>Manual tonemap control<ul> + * <li>{@link CaptureRequest#TONEMAP_CURVE_BLUE android.tonemap.curveBlue}</li> + * <li>{@link CaptureRequest#TONEMAP_CURVE_GREEN android.tonemap.curveGreen}</li> + * <li>{@link CaptureRequest#TONEMAP_CURVE_RED android.tonemap.curveRed}</li> + * <li>{@link CaptureRequest#TONEMAP_MODE android.tonemap.mode}</li> + * <li>{@link CameraCharacteristics#TONEMAP_MAX_CURVE_POINTS android.tonemap.maxCurvePoints}</li> + * </ul> + * </li> + * <li>Manual white balance control<ul> + * <li>{@link CaptureRequest#COLOR_CORRECTION_TRANSFORM android.colorCorrection.transform}</li> + * <li>{@link CaptureRequest#COLOR_CORRECTION_GAINS android.colorCorrection.gains}</li> + * </ul> + * </li> + * <li>Lens shading map information<ul> + * <li>{@link CaptureResult#STATISTICS_LENS_SHADING_MAP android.statistics.lensShadingMap}</li> + * <li>{@link CameraCharacteristics#LENS_INFO_SHADING_MAP_SIZE android.lens.info.shadingMapSize}</li> + * </ul> + * </li> + * </ul> + * <p>If auto white balance is enabled, then the camera device + * will accurately report the values applied by AWB in the result.</p> + * <p>The camera device will also support everything in MANUAL_SENSOR + * except manual lens control and manual flash control.</p> + * + * @see CaptureRequest#COLOR_CORRECTION_GAINS + * @see CaptureRequest#COLOR_CORRECTION_TRANSFORM + * @see CameraCharacteristics#LENS_INFO_SHADING_MAP_SIZE + * @see CaptureResult#STATISTICS_LENS_SHADING_MAP + * @see CaptureRequest#TONEMAP_CURVE_BLUE + * @see CaptureRequest#TONEMAP_CURVE_GREEN + * @see CaptureRequest#TONEMAP_CURVE_RED + * @see CameraCharacteristics#TONEMAP_MAX_CURVE_POINTS + * @see CaptureRequest#TONEMAP_MODE + * @see CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES + */ + public static final int REQUEST_AVAILABLE_CAPABILITIES_GCAM = 3; + + /** + * <p>The camera device supports the Zero Shutter Lag use case.</p> + * <ul> + * <li>At least one input stream can be used.</li> + * <li>RAW_OPAQUE is supported as an output/input format</li> + * <li>Using RAW_OPAQUE does not cause a frame rate drop + * relative to the sensor's maximum capture rate (at that + * resolution).</li> + * <li>RAW_OPAQUE will be reprocessable into both YUV_420_888 + * and JPEG formats.</li> + * <li>The maximum available resolution for RAW_OPAQUE streams + * (both input/output) will match the maximum available + * resolution of JPEG streams.</li> + * </ul> + * @see CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES + */ + public static final int REQUEST_AVAILABLE_CAPABILITIES_ZSL = 4; + + /** + * <p>The camera device supports outputting RAW buffers that can be + * saved offline into a DNG format. It can reprocess DNG + * files (produced from the same camera device) back into YUV.</p> + * <ul> + * <li>At least one input stream can be used.</li> + * <li>RAW16 is supported as output/input format.</li> + * <li>RAW16 is reprocessable into both YUV_420_888 and JPEG + * formats.</li> + * <li>The maximum available resolution for RAW16 streams (both + * input/output) will match the value in + * {@link CameraCharacteristics#SENSOR_INFO_PIXEL_ARRAY_SIZE android.sensor.info.pixelArraySize}.</li> + * <li>All DNG-related optional metadata entries are provided + * by the camera device.</li> + * </ul> + * + * @see CameraCharacteristics#SENSOR_INFO_PIXEL_ARRAY_SIZE + * @see CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES + */ + public static final int REQUEST_AVAILABLE_CAPABILITIES_DNG = 5; + + // + // Enumeration values for CameraCharacteristics#SCALER_AVAILABLE_STREAM_CONFIGURATIONS + // + + /** + * @see CameraCharacteristics#SCALER_AVAILABLE_STREAM_CONFIGURATIONS + */ + public static final int SCALER_AVAILABLE_STREAM_CONFIGURATIONS_OUTPUT = 0; + + /** + * @see CameraCharacteristics#SCALER_AVAILABLE_STREAM_CONFIGURATIONS + */ + public static final int SCALER_AVAILABLE_STREAM_CONFIGURATIONS_INPUT = 1; + + // + // Enumeration values for CameraCharacteristics#SENSOR_INFO_COLOR_FILTER_ARRANGEMENT + // + + /** + * @see CameraCharacteristics#SENSOR_INFO_COLOR_FILTER_ARRANGEMENT + */ + public static final int SENSOR_INFO_COLOR_FILTER_ARRANGEMENT_RGGB = 0; + + /** + * @see CameraCharacteristics#SENSOR_INFO_COLOR_FILTER_ARRANGEMENT + */ + public static final int SENSOR_INFO_COLOR_FILTER_ARRANGEMENT_GRBG = 1; + + /** + * @see CameraCharacteristics#SENSOR_INFO_COLOR_FILTER_ARRANGEMENT + */ + public static final int SENSOR_INFO_COLOR_FILTER_ARRANGEMENT_GBRG = 2; + + /** + * @see CameraCharacteristics#SENSOR_INFO_COLOR_FILTER_ARRANGEMENT + */ + public static final int SENSOR_INFO_COLOR_FILTER_ARRANGEMENT_BGGR = 3; + + /** + * <p>Sensor is not Bayer; output has 3 16-bit + * values for each pixel, instead of just 1 16-bit value + * per pixel.</p> + * @see CameraCharacteristics#SENSOR_INFO_COLOR_FILTER_ARRANGEMENT + */ + public static final int SENSOR_INFO_COLOR_FILTER_ARRANGEMENT_RGB = 4; + + // // Enumeration values for CameraCharacteristics#LED_AVAILABLE_LEDS // /** - * <p> - * android.led.transmit control is used - * </p> + * <p>android.led.transmit control is used</p> * @see CameraCharacteristics#LED_AVAILABLE_LEDS * @hide */ @@ -247,32 +482,75 @@ public abstract class CameraMetadata { public static final int INFO_SUPPORTED_HARDWARE_LEVEL_FULL = 1; // + // Enumeration values for CameraCharacteristics#SYNC_MAX_LATENCY + // + + /** + * <p>Every frame has the requests immediately applied. + * (and furthermore for all results, + * <code>android.sync.frameNumber == android.request.frameCount</code>)</p> + * <p>Changing controls over multiple requests one after another will + * produce results that have those controls applied atomically + * each frame.</p> + * <p>All FULL capability devices will have this as their maxLatency.</p> + * @see CameraCharacteristics#SYNC_MAX_LATENCY + */ + public static final int SYNC_MAX_LATENCY_PER_FRAME_CONTROL = 0; + + /** + * <p>Each new frame has some subset (potentially the entire set) + * of the past requests applied to the camera settings.</p> + * <p>By submitting a series of identical requests, the camera device + * will eventually have the camera settings applied, but it is + * unknown when that exact point will be.</p> + * @see CameraCharacteristics#SYNC_MAX_LATENCY + */ + public static final int SYNC_MAX_LATENCY_UNKNOWN = -1; + + // // Enumeration values for CaptureRequest#COLOR_CORRECTION_MODE // /** - * <p> - * Use the android.colorCorrection.transform matrix - * and android.colorCorrection.gains to do color conversion - * </p> + * <p>Use the {@link CaptureRequest#COLOR_CORRECTION_TRANSFORM android.colorCorrection.transform} matrix + * and {@link CaptureRequest#COLOR_CORRECTION_GAINS android.colorCorrection.gains} to do color conversion.</p> + * <p>All advanced white balance adjustments (not specified + * by our white balance pipeline) must be disabled.</p> + * <p>If AWB is enabled with <code>{@link CaptureRequest#CONTROL_AWB_MODE android.control.awbMode} != OFF</code>, then + * TRANSFORM_MATRIX is ignored. The camera device will override + * this value to either FAST or HIGH_QUALITY.</p> + * + * @see CaptureRequest#COLOR_CORRECTION_GAINS + * @see CaptureRequest#COLOR_CORRECTION_TRANSFORM + * @see CaptureRequest#CONTROL_AWB_MODE * @see CaptureRequest#COLOR_CORRECTION_MODE */ public static final int COLOR_CORRECTION_MODE_TRANSFORM_MATRIX = 0; /** - * <p> - * Must not slow down frame rate relative to raw - * bayer output - * </p> + * <p>Must not slow down capture rate relative to sensor raw + * output.</p> + * <p>Advanced white balance adjustments above and beyond + * the specified white balance pipeline may be applied.</p> + * <p>If AWB is enabled with <code>{@link CaptureRequest#CONTROL_AWB_MODE android.control.awbMode} != OFF</code>, then + * the camera device uses the last frame's AWB values + * (or defaults if AWB has never been run).</p> + * + * @see CaptureRequest#CONTROL_AWB_MODE * @see CaptureRequest#COLOR_CORRECTION_MODE */ public static final int COLOR_CORRECTION_MODE_FAST = 1; /** - * <p> - * Frame rate may be reduced by high - * quality - * </p> + * <p>Capture rate (relative to sensor raw output) + * may be reduced by high quality.</p> + * <p>Advanced white balance adjustments above and beyond + * the specified white balance pipeline may be applied.</p> + * <p>If AWB is enabled with <code>{@link CaptureRequest#CONTROL_AWB_MODE android.control.awbMode} != OFF</code>, then + * the camera device uses the last frame's AWB values + * (or defaults if AWB has never been run).</p> + * + * @see CaptureRequest#CONTROL_AWB_MODE * @see CaptureRequest#COLOR_CORRECTION_MODE */ public static final int COLOR_CORRECTION_MODE_HIGH_QUALITY = 2; @@ -282,21 +560,31 @@ public abstract class CameraMetadata { // /** + * <p>The camera device will not adjust exposure duration to + * avoid banding problems.</p> * @see CaptureRequest#CONTROL_AE_ANTIBANDING_MODE */ public static final int CONTROL_AE_ANTIBANDING_MODE_OFF = 0; /** + * <p>The camera device will adjust exposure duration to + * avoid banding problems with 50Hz illumination sources.</p> * @see CaptureRequest#CONTROL_AE_ANTIBANDING_MODE */ public static final int CONTROL_AE_ANTIBANDING_MODE_50HZ = 1; /** + * <p>The camera device will adjust exposure duration to + * avoid banding problems with 60Hz illumination + * sources.</p> * @see CaptureRequest#CONTROL_AE_ANTIBANDING_MODE */ public static final int CONTROL_AE_ANTIBANDING_MODE_60HZ = 2; /** + * <p>The camera device will automatically adapt its + * antibanding routine to the current illumination + * conditions. This is the default.</p> * @see CaptureRequest#CONTROL_AE_ANTIBANDING_MODE */ public static final int CONTROL_AE_ANTIBANDING_MODE_AUTO = 3; @@ -306,52 +594,73 @@ public abstract class CameraMetadata { // /** - * <p> - * Autoexposure is disabled; sensor.exposureTime, - * sensor.sensitivity and sensor.frameDuration are used - * </p> + * <p>The camera device's autoexposure routine is disabled; + * the application-selected {@link CaptureRequest#SENSOR_EXPOSURE_TIME android.sensor.exposureTime}, + * {@link CaptureRequest#SENSOR_SENSITIVITY android.sensor.sensitivity} and + * {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration} are used by the camera + * device, along with android.flash.* fields, if there's + * a flash unit for this camera device.</p> + * + * @see CaptureRequest#SENSOR_EXPOSURE_TIME + * @see CaptureRequest#SENSOR_FRAME_DURATION + * @see CaptureRequest#SENSOR_SENSITIVITY * @see CaptureRequest#CONTROL_AE_MODE */ public static final int CONTROL_AE_MODE_OFF = 0; /** - * <p> - * Autoexposure is active, no flash - * control - * </p> + * <p>The camera device's autoexposure routine is active, + * with no flash control. The application's values for + * {@link CaptureRequest#SENSOR_EXPOSURE_TIME android.sensor.exposureTime}, + * {@link CaptureRequest#SENSOR_SENSITIVITY android.sensor.sensitivity}, and + * {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration} are ignored. The + * application has control over the various + * android.flash.* fields.</p> + * + * @see CaptureRequest#SENSOR_EXPOSURE_TIME + * @see CaptureRequest#SENSOR_FRAME_DURATION + * @see CaptureRequest#SENSOR_SENSITIVITY * @see CaptureRequest#CONTROL_AE_MODE */ public static final int CONTROL_AE_MODE_ON = 1; /** - * <p> - * if flash exists Autoexposure is active, auto - * flash control; flash may be fired when precapture - * trigger is activated, and for captures for which - * captureIntent = STILL_CAPTURE - * </p> + * <p>Like ON, except that the camera device also controls + * the camera's flash unit, firing it in low-light + * conditions. The flash may be fired during a + * precapture sequence (triggered by + * {@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER android.control.aePrecaptureTrigger}) and may be fired + * for captures for which the + * {@link CaptureRequest#CONTROL_CAPTURE_INTENT android.control.captureIntent} field is set to + * STILL_CAPTURE</p> + * + * @see CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER + * @see CaptureRequest#CONTROL_CAPTURE_INTENT * @see CaptureRequest#CONTROL_AE_MODE */ public static final int CONTROL_AE_MODE_ON_AUTO_FLASH = 2; /** - * <p> - * if flash exists Autoexposure is active, auto - * flash control for precapture trigger and always flash - * when captureIntent = STILL_CAPTURE - * </p> + * <p>Like ON, except that the camera device also controls + * the camera's flash unit, always firing it for still + * captures. The flash may be fired during a precapture + * sequence (triggered by + * {@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER android.control.aePrecaptureTrigger}) and will always + * be fired for captures for which the + * {@link CaptureRequest#CONTROL_CAPTURE_INTENT android.control.captureIntent} field is set to + * STILL_CAPTURE</p> + * + * @see CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER + * @see CaptureRequest#CONTROL_CAPTURE_INTENT * @see CaptureRequest#CONTROL_AE_MODE */ public static final int CONTROL_AE_MODE_ON_ALWAYS_FLASH = 3; /** - * <p> - * optional Automatic red eye reduction with flash. - * If deemed necessary, red eye reduction sequence should - * fire when precapture trigger is activated, and final - * flash should fire when captureIntent = - * STILL_CAPTURE - * </p> + * <p>Like ON_AUTO_FLASH, but with automatic red eye + * reduction. If deemed necessary by the camera device, + * a red eye reduction flash will fire during the + * precapture sequence.</p> * @see CaptureRequest#CONTROL_AE_MODE */ public static final int CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE = 4; @@ -361,20 +670,15 @@ public abstract class CameraMetadata { // /** - * <p> - * The trigger is idle. - * </p> + * <p>The trigger is idle.</p> * @see CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER */ public static final int CONTROL_AE_PRECAPTURE_TRIGGER_IDLE = 0; /** - * <p> - * The precapture metering sequence - * must be started. The exact effect of the precapture - * trigger depends on the current AE mode and - * state. - * </p> + * <p>The precapture metering sequence will be started + * by the camera device. The exact effect of the precapture + * trigger depends on the current AE mode and state.</p> * @see CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER */ public static final int CONTROL_AE_PRECAPTURE_TRIGGER_START = 1; @@ -384,55 +688,47 @@ public abstract class CameraMetadata { // /** - * <p> - * The 3A routines do not control the lens; - * android.lens.focusDistance is controlled by the - * application - * </p> + * <p>The auto-focus routine does not control the lens; + * {@link CaptureRequest#LENS_FOCUS_DISTANCE android.lens.focusDistance} is controlled by the + * application</p> + * + * @see CaptureRequest#LENS_FOCUS_DISTANCE * @see CaptureRequest#CONTROL_AF_MODE */ public static final int CONTROL_AF_MODE_OFF = 0; /** - * <p> - * if lens is not fixed focus. - * </p><p> - * Use android.lens.minimumFocusDistance to determine if lens - * is fixed focus In this mode, the lens does not move unless + * <p>If lens is not fixed focus.</p> + * <p>Use {@link CameraCharacteristics#LENS_INFO_MINIMUM_FOCUS_DISTANCE android.lens.info.minimumFocusDistance} to determine if lens + * is fixed-focus. In this mode, the lens does not move unless * the autofocus trigger action is called. When that trigger * is activated, AF must transition to ACTIVE_SCAN, then to - * the outcome of the scan (FOCUSED or - * NOT_FOCUSED). - * </p><p> - * Triggering cancel AF resets the lens position to default, - * and sets the AF state to INACTIVE. - * </p> + * the outcome of the scan (FOCUSED or NOT_FOCUSED).</p> + * <p>Triggering AF_CANCEL resets the lens position to default, + * and sets the AF state to INACTIVE.</p> + * + * @see CameraCharacteristics#LENS_INFO_MINIMUM_FOCUS_DISTANCE * @see CaptureRequest#CONTROL_AF_MODE */ public static final int CONTROL_AF_MODE_AUTO = 1; /** - * <p> - * In this mode, the lens does not move unless the - * autofocus trigger action is called. - * </p><p> - * When that trigger is activated, AF must transition to + * <p>In this mode, the lens does not move unless the + * autofocus trigger action is called.</p> + * <p>When that trigger is activated, AF must transition to * ACTIVE_SCAN, then to the outcome of the scan (FOCUSED or * NOT_FOCUSED). Triggering cancel AF resets the lens * position to default, and sets the AF state to - * INACTIVE. - * </p> + * INACTIVE.</p> * @see CaptureRequest#CONTROL_AF_MODE */ public static final int CONTROL_AF_MODE_MACRO = 2; /** - * <p> - * In this mode, the AF algorithm modifies the lens + * <p>In this mode, the AF algorithm modifies the lens * position continually to attempt to provide a - * constantly-in-focus image stream. - * </p><p> - * The focusing behavior should be suitable for good quality + * constantly-in-focus image stream.</p> + * <p>The focusing behavior should be suitable for good quality * video recording; typically this means slower focus * movement and no overshoots. When the AF trigger is not * involved, the AF algorithm should start in INACTIVE state, @@ -440,25 +736,21 @@ public abstract class CameraMetadata { * states as appropriate. When the AF trigger is activated, * the algorithm should immediately transition into * AF_FOCUSED or AF_NOT_FOCUSED as appropriate, and lock the - * lens position until a cancel AF trigger is received. - * </p><p> - * Once cancel is received, the algorithm should transition + * lens position until a cancel AF trigger is received.</p> + * <p>Once cancel is received, the algorithm should transition * back to INACTIVE and resume passive scan. Note that this * behavior is not identical to CONTINUOUS_PICTURE, since an * ongoing PASSIVE_SCAN must immediately be - * canceled. - * </p> + * canceled.</p> * @see CaptureRequest#CONTROL_AF_MODE */ public static final int CONTROL_AF_MODE_CONTINUOUS_VIDEO = 3; /** - * <p> - * In this mode, the AF algorithm modifies the lens + * <p>In this mode, the AF algorithm modifies the lens * position continually to attempt to provide a - * constantly-in-focus image stream. - * </p><p> - * The focusing behavior should be suitable for still image + * constantly-in-focus image stream.</p> + * <p>The focusing behavior should be suitable for still image * capture; typically this means focusing as fast as * possible. When the AF trigger is not involved, the AF * algorithm should start in INACTIVE state, and then @@ -467,22 +759,18 @@ public abstract class CameraMetadata { * trigger is activated, the algorithm should finish its * PASSIVE_SCAN if active, and then transition into * AF_FOCUSED or AF_NOT_FOCUSED as appropriate, and lock the - * lens position until a cancel AF trigger is received. - * </p><p> - * When the AF cancel trigger is activated, the algorithm + * lens position until a cancel AF trigger is received.</p> + * <p>When the AF cancel trigger is activated, the algorithm * should transition back to INACTIVE and then act as if it - * has just been started. - * </p> + * has just been started.</p> * @see CaptureRequest#CONTROL_AF_MODE */ public static final int CONTROL_AF_MODE_CONTINUOUS_PICTURE = 4; /** - * <p> - * Extended depth of field (digital focus). AF + * <p>Extended depth of field (digital focus). AF * trigger is ignored, AF state should always be - * INACTIVE. - * </p> + * INACTIVE.</p> * @see CaptureRequest#CONTROL_AF_MODE */ public static final int CONTROL_AF_MODE_EDOF = 5; @@ -492,26 +780,20 @@ public abstract class CameraMetadata { // /** - * <p> - * The trigger is idle. - * </p> + * <p>The trigger is idle.</p> * @see CaptureRequest#CONTROL_AF_TRIGGER */ public static final int CONTROL_AF_TRIGGER_IDLE = 0; /** - * <p> - * Autofocus must trigger now. - * </p> + * <p>Autofocus will trigger now.</p> * @see CaptureRequest#CONTROL_AF_TRIGGER */ public static final int CONTROL_AF_TRIGGER_START = 1; /** - * <p> - * Autofocus must return to initial - * state, and cancel any active trigger. - * </p> + * <p>Autofocus will return to its initial + * state, and cancel any currently active trigger.</p> * @see CaptureRequest#CONTROL_AF_TRIGGER */ public static final int CONTROL_AF_TRIGGER_CANCEL = 2; @@ -521,46 +803,89 @@ public abstract class CameraMetadata { // /** + * <p>The camera device's auto white balance routine is disabled; + * the application-selected color transform matrix + * ({@link CaptureRequest#COLOR_CORRECTION_TRANSFORM android.colorCorrection.transform}) and gains + * ({@link CaptureRequest#COLOR_CORRECTION_GAINS android.colorCorrection.gains}) are used by the camera + * device for manual white balance control.</p> + * + * @see CaptureRequest#COLOR_CORRECTION_GAINS + * @see CaptureRequest#COLOR_CORRECTION_TRANSFORM * @see CaptureRequest#CONTROL_AWB_MODE */ public static final int CONTROL_AWB_MODE_OFF = 0; /** + * <p>The camera device's auto white balance routine is active; + * the application's values for {@link CaptureRequest#COLOR_CORRECTION_TRANSFORM android.colorCorrection.transform} + * and {@link CaptureRequest#COLOR_CORRECTION_GAINS android.colorCorrection.gains} are ignored.</p> + * + * @see CaptureRequest#COLOR_CORRECTION_GAINS + * @see CaptureRequest#COLOR_CORRECTION_TRANSFORM * @see CaptureRequest#CONTROL_AWB_MODE */ public static final int CONTROL_AWB_MODE_AUTO = 1; /** + * <p>The camera device's auto white balance routine is disabled; + * the camera device uses incandescent light as the assumed scene + * illumination for white balance. While the exact white balance + * transforms are up to the camera device, they will approximately + * match the CIE standard illuminant A.</p> * @see CaptureRequest#CONTROL_AWB_MODE */ public static final int CONTROL_AWB_MODE_INCANDESCENT = 2; /** + * <p>The camera device's auto white balance routine is disabled; + * the camera device uses fluorescent light as the assumed scene + * illumination for white balance. While the exact white balance + * transforms are up to the camera device, they will approximately + * match the CIE standard illuminant F2.</p> * @see CaptureRequest#CONTROL_AWB_MODE */ public static final int CONTROL_AWB_MODE_FLUORESCENT = 3; /** + * <p>The camera device's auto white balance routine is disabled; + * the camera device uses warm fluorescent light as the assumed scene + * illumination for white balance. While the exact white balance + * transforms are up to the camera device, they will approximately + * match the CIE standard illuminant F4.</p> * @see CaptureRequest#CONTROL_AWB_MODE */ public static final int CONTROL_AWB_MODE_WARM_FLUORESCENT = 4; /** + * <p>The camera device's auto white balance routine is disabled; + * the camera device uses daylight light as the assumed scene + * illumination for white balance. While the exact white balance + * transforms are up to the camera device, they will approximately + * match the CIE standard illuminant D65.</p> * @see CaptureRequest#CONTROL_AWB_MODE */ public static final int CONTROL_AWB_MODE_DAYLIGHT = 5; /** + * <p>The camera device's auto white balance routine is disabled; + * the camera device uses cloudy daylight light as the assumed scene + * illumination for white balance.</p> * @see CaptureRequest#CONTROL_AWB_MODE */ public static final int CONTROL_AWB_MODE_CLOUDY_DAYLIGHT = 6; /** + * <p>The camera device's auto white balance routine is disabled; + * the camera device uses twilight light as the assumed scene + * illumination for white balance.</p> * @see CaptureRequest#CONTROL_AWB_MODE */ public static final int CONTROL_AWB_MODE_TWILIGHT = 7; /** + * <p>The camera device's auto white balance routine is disabled; + * the camera device uses shade light as the assumed scene + * illumination for white balance.</p> * @see CaptureRequest#CONTROL_AWB_MODE */ public static final int CONTROL_AWB_MODE_SHADE = 8; @@ -570,108 +895,125 @@ public abstract class CameraMetadata { // /** - * <p> - * This request doesn't fall into the other + * <p>This request doesn't fall into the other * categories. Default to preview-like - * behavior. - * </p> + * behavior.</p> * @see CaptureRequest#CONTROL_CAPTURE_INTENT */ public static final int CONTROL_CAPTURE_INTENT_CUSTOM = 0; /** - * <p> - * This request is for a preview-like usecase. The + * <p>This request is for a preview-like usecase. The * precapture trigger may be used to start off a metering - * w/flash sequence - * </p> + * w/flash sequence</p> * @see CaptureRequest#CONTROL_CAPTURE_INTENT */ public static final int CONTROL_CAPTURE_INTENT_PREVIEW = 1; /** - * <p> - * This request is for a still capture-type - * usecase. - * </p> + * <p>This request is for a still capture-type + * usecase.</p> * @see CaptureRequest#CONTROL_CAPTURE_INTENT */ public static final int CONTROL_CAPTURE_INTENT_STILL_CAPTURE = 2; /** - * <p> - * This request is for a video recording - * usecase. - * </p> + * <p>This request is for a video recording + * usecase.</p> * @see CaptureRequest#CONTROL_CAPTURE_INTENT */ public static final int CONTROL_CAPTURE_INTENT_VIDEO_RECORD = 3; /** - * <p> - * This request is for a video snapshot (still - * image while recording video) usecase - * </p> + * <p>This request is for a video snapshot (still + * image while recording video) usecase</p> * @see CaptureRequest#CONTROL_CAPTURE_INTENT */ public static final int CONTROL_CAPTURE_INTENT_VIDEO_SNAPSHOT = 4; /** - * <p> - * This request is for a ZSL usecase; the + * <p>This request is for a ZSL usecase; the * application will stream full-resolution images and * reprocess one or several later for a final - * capture - * </p> + * capture</p> * @see CaptureRequest#CONTROL_CAPTURE_INTENT */ public static final int CONTROL_CAPTURE_INTENT_ZERO_SHUTTER_LAG = 5; + /** + * <p>This request is for manual capture use case where + * the applications want to directly control the capture parameters + * (e.g. {@link CaptureRequest#SENSOR_EXPOSURE_TIME android.sensor.exposureTime}, {@link CaptureRequest#SENSOR_SENSITIVITY android.sensor.sensitivity} etc.).</p> + * + * @see CaptureRequest#SENSOR_EXPOSURE_TIME + * @see CaptureRequest#SENSOR_SENSITIVITY + * @see CaptureRequest#CONTROL_CAPTURE_INTENT + */ + public static final int CONTROL_CAPTURE_INTENT_MANUAL = 6; + // // Enumeration values for CaptureRequest#CONTROL_EFFECT_MODE // /** + * <p>No color effect will be applied.</p> * @see CaptureRequest#CONTROL_EFFECT_MODE */ public static final int CONTROL_EFFECT_MODE_OFF = 0; /** + * <p>A "monocolor" effect where the image is mapped into + * a single color. This will typically be grayscale.</p> * @see CaptureRequest#CONTROL_EFFECT_MODE */ public static final int CONTROL_EFFECT_MODE_MONO = 1; /** + * <p>A "photo-negative" effect where the image's colors + * are inverted.</p> * @see CaptureRequest#CONTROL_EFFECT_MODE */ public static final int CONTROL_EFFECT_MODE_NEGATIVE = 2; /** + * <p>A "solarisation" effect (Sabattier effect) where the + * image is wholly or partially reversed in + * tone.</p> * @see CaptureRequest#CONTROL_EFFECT_MODE */ public static final int CONTROL_EFFECT_MODE_SOLARIZE = 3; /** + * <p>A "sepia" effect where the image is mapped into warm + * gray, red, and brown tones.</p> * @see CaptureRequest#CONTROL_EFFECT_MODE */ public static final int CONTROL_EFFECT_MODE_SEPIA = 4; /** + * <p>A "posterization" effect where the image uses + * discrete regions of tone rather than a continuous + * gradient of tones.</p> * @see CaptureRequest#CONTROL_EFFECT_MODE */ public static final int CONTROL_EFFECT_MODE_POSTERIZE = 5; /** + * <p>A "whiteboard" effect where the image is typically displayed + * as regions of white, with black or grey details.</p> * @see CaptureRequest#CONTROL_EFFECT_MODE */ public static final int CONTROL_EFFECT_MODE_WHITEBOARD = 6; /** + * <p>A "blackboard" effect where the image is typically displayed + * as regions of black, with white or grey details.</p> * @see CaptureRequest#CONTROL_EFFECT_MODE */ public static final int CONTROL_EFFECT_MODE_BLACKBOARD = 7; /** + * <p>An "aqua" effect where a blue hue is added to the image.</p> * @see CaptureRequest#CONTROL_EFFECT_MODE */ public static final int CONTROL_EFFECT_MODE_AQUA = 8; @@ -681,137 +1023,169 @@ public abstract class CameraMetadata { // /** - * <p> - * Full application control of pipeline. All 3A + * <p>Full application control of pipeline. All 3A * routines are disabled, no other settings in - * android.control.* have any effect - * </p> + * android.control.* have any effect</p> * @see CaptureRequest#CONTROL_MODE */ public static final int CONTROL_MODE_OFF = 0; /** - * <p> - * Use settings for each individual 3A routine. + * <p>Use settings for each individual 3A routine. * Manual control of capture parameters is disabled. All * controls in android.control.* besides sceneMode take - * effect - * </p> + * effect</p> * @see CaptureRequest#CONTROL_MODE */ public static final int CONTROL_MODE_AUTO = 1; /** - * <p> - * Use specific scene mode. Enabling this disables + * <p>Use specific scene mode. Enabling this disables * control.aeMode, control.awbMode and control.afMode - * controls; the HAL must ignore those settings while + * controls; the camera device will ignore those settings while * USE_SCENE_MODE is active (except for FACE_PRIORITY * scene mode). Other control entries are still active. - * This setting can only be used if availableSceneModes != - * UNSUPPORTED - * </p> + * This setting can only be used if scene mode is supported + * (i.e. {@link CameraCharacteristics#CONTROL_AVAILABLE_SCENE_MODES android.control.availableSceneModes} contain some modes + * other than DISABLED).</p> + * + * @see CameraCharacteristics#CONTROL_AVAILABLE_SCENE_MODES * @see CaptureRequest#CONTROL_MODE */ public static final int CONTROL_MODE_USE_SCENE_MODE = 2; + /** + * <p>Same as OFF mode, except that this capture will not be + * used by camera device background auto-exposure, auto-white balance and + * auto-focus algorithms to update their statistics.</p> + * @see CaptureRequest#CONTROL_MODE + */ + public static final int CONTROL_MODE_OFF_KEEP_STATE = 3; + // // Enumeration values for CaptureRequest#CONTROL_SCENE_MODE // /** + * <p>Indicates that no scene modes are set for a given capture request.</p> * @see CaptureRequest#CONTROL_SCENE_MODE */ - public static final int CONTROL_SCENE_MODE_UNSUPPORTED = 0; + public static final int CONTROL_SCENE_MODE_DISABLED = 0; /** - * <p> - * if face detection support exists Use face - * detection data to drive 3A routines. If face detection - * statistics are disabled, should still operate correctly - * (but not return face detection statistics to the - * framework). - * </p><p> - * Unlike the other scene modes, aeMode, awbMode, and afMode - * remain active when FACE_PRIORITY is set. This is due to - * compatibility concerns with the old camera - * API - * </p> + * <p>If face detection support exists, use face + * detection data for auto-focus, auto-white balance, and + * auto-exposure routines. If face detection statistics are + * disabled (i.e. {@link CaptureRequest#STATISTICS_FACE_DETECT_MODE android.statistics.faceDetectMode} is set to OFF), + * this should still operate correctly (but will not return + * face detection statistics to the framework).</p> + * <p>Unlike the other scene modes, {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode}, + * {@link CaptureRequest#CONTROL_AWB_MODE android.control.awbMode}, and {@link CaptureRequest#CONTROL_AF_MODE android.control.afMode} + * remain active when FACE_PRIORITY is set.</p> + * + * @see CaptureRequest#CONTROL_AE_MODE + * @see CaptureRequest#CONTROL_AF_MODE + * @see CaptureRequest#CONTROL_AWB_MODE + * @see CaptureRequest#STATISTICS_FACE_DETECT_MODE * @see CaptureRequest#CONTROL_SCENE_MODE */ public static final int CONTROL_SCENE_MODE_FACE_PRIORITY = 1; /** + * <p>Optimized for photos of quickly moving objects. + * Similar to SPORTS.</p> * @see CaptureRequest#CONTROL_SCENE_MODE */ public static final int CONTROL_SCENE_MODE_ACTION = 2; /** + * <p>Optimized for still photos of people.</p> * @see CaptureRequest#CONTROL_SCENE_MODE */ public static final int CONTROL_SCENE_MODE_PORTRAIT = 3; /** + * <p>Optimized for photos of distant macroscopic objects.</p> * @see CaptureRequest#CONTROL_SCENE_MODE */ public static final int CONTROL_SCENE_MODE_LANDSCAPE = 4; /** + * <p>Optimized for low-light settings.</p> * @see CaptureRequest#CONTROL_SCENE_MODE */ public static final int CONTROL_SCENE_MODE_NIGHT = 5; /** + * <p>Optimized for still photos of people in low-light + * settings.</p> * @see CaptureRequest#CONTROL_SCENE_MODE */ public static final int CONTROL_SCENE_MODE_NIGHT_PORTRAIT = 6; /** + * <p>Optimized for dim, indoor settings where flash must + * remain off.</p> * @see CaptureRequest#CONTROL_SCENE_MODE */ public static final int CONTROL_SCENE_MODE_THEATRE = 7; /** + * <p>Optimized for bright, outdoor beach settings.</p> * @see CaptureRequest#CONTROL_SCENE_MODE */ public static final int CONTROL_SCENE_MODE_BEACH = 8; /** + * <p>Optimized for bright, outdoor settings containing snow.</p> * @see CaptureRequest#CONTROL_SCENE_MODE */ public static final int CONTROL_SCENE_MODE_SNOW = 9; /** + * <p>Optimized for scenes of the setting sun.</p> * @see CaptureRequest#CONTROL_SCENE_MODE */ public static final int CONTROL_SCENE_MODE_SUNSET = 10; /** + * <p>Optimized to avoid blurry photos due to small amounts of + * device motion (for example: due to hand shake).</p> * @see CaptureRequest#CONTROL_SCENE_MODE */ public static final int CONTROL_SCENE_MODE_STEADYPHOTO = 11; /** + * <p>Optimized for nighttime photos of fireworks.</p> * @see CaptureRequest#CONTROL_SCENE_MODE */ public static final int CONTROL_SCENE_MODE_FIREWORKS = 12; /** + * <p>Optimized for photos of quickly moving people. + * Similar to ACTION.</p> * @see CaptureRequest#CONTROL_SCENE_MODE */ public static final int CONTROL_SCENE_MODE_SPORTS = 13; /** + * <p>Optimized for dim, indoor settings with multiple moving + * people.</p> * @see CaptureRequest#CONTROL_SCENE_MODE */ public static final int CONTROL_SCENE_MODE_PARTY = 14; /** + * <p>Optimized for dim settings where the main light source + * is a flame.</p> * @see CaptureRequest#CONTROL_SCENE_MODE */ public static final int CONTROL_SCENE_MODE_CANDLELIGHT = 15; /** + * <p>Optimized for accurately capturing a photo of barcode + * for use by camera applications that wish to read the + * barcode value.</p> * @see CaptureRequest#CONTROL_SCENE_MODE */ public static final int CONTROL_SCENE_MODE_BARCODE = 16; @@ -821,27 +1195,21 @@ public abstract class CameraMetadata { // /** - * <p> - * No edge enhancement is applied - * </p> + * <p>No edge enhancement is applied</p> * @see CaptureRequest#EDGE_MODE */ public static final int EDGE_MODE_OFF = 0; /** - * <p> - * Must not slow down frame rate relative to raw - * bayer output - * </p> + * <p>Must not slow down frame rate relative to sensor + * output</p> * @see CaptureRequest#EDGE_MODE */ public static final int EDGE_MODE_FAST = 1; /** - * <p> - * Frame rate may be reduced by high - * quality - * </p> + * <p>Frame rate may be reduced by high + * quality</p> * @see CaptureRequest#EDGE_MODE */ public static final int EDGE_MODE_HIGH_QUALITY = 2; @@ -851,44 +1219,74 @@ public abstract class CameraMetadata { // /** - * <p> - * Do not fire the flash for this - * capture - * </p> + * <p>Do not fire the flash for this capture.</p> * @see CaptureRequest#FLASH_MODE */ public static final int FLASH_MODE_OFF = 0; /** - * <p> - * if android.flash.available is true Fire flash - * for this capture based on firingPower, - * firingTime. - * </p> + * <p>If the flash is available and charged, fire flash + * for this capture based on android.flash.firingPower and + * android.flash.firingTime.</p> * @see CaptureRequest#FLASH_MODE */ public static final int FLASH_MODE_SINGLE = 1; /** - * <p> - * if android.flash.available is true Flash - * continuously on, power set by - * firingPower - * </p> + * <p>Transition flash to continuously on.</p> * @see CaptureRequest#FLASH_MODE */ public static final int FLASH_MODE_TORCH = 2; // + // Enumeration values for CaptureRequest#HOT_PIXEL_MODE + // + + /** + * <p>The frame rate must not be reduced relative to sensor raw output + * for this option.</p> + * <p>No hot pixel correction is applied. + * The hotpixel map may be returned in {@link CaptureResult#STATISTICS_HOT_PIXEL_MAP android.statistics.hotPixelMap}.</p> + * + * @see CaptureResult#STATISTICS_HOT_PIXEL_MAP + * @see CaptureRequest#HOT_PIXEL_MODE + */ + public static final int HOT_PIXEL_MODE_OFF = 0; + + /** + * <p>The frame rate must not be reduced relative to sensor raw output + * for this option.</p> + * <p>Hot pixel correction is applied. + * The hotpixel map may be returned in {@link CaptureResult#STATISTICS_HOT_PIXEL_MAP android.statistics.hotPixelMap}.</p> + * + * @see CaptureResult#STATISTICS_HOT_PIXEL_MAP + * @see CaptureRequest#HOT_PIXEL_MODE + */ + public static final int HOT_PIXEL_MODE_FAST = 1; + + /** + * <p>The frame rate may be reduced relative to sensor raw output + * for this option.</p> + * <p>A high-quality hot pixel correction is applied. + * The hotpixel map may be returned in {@link CaptureResult#STATISTICS_HOT_PIXEL_MAP android.statistics.hotPixelMap}.</p> + * + * @see CaptureResult#STATISTICS_HOT_PIXEL_MAP + * @see CaptureRequest#HOT_PIXEL_MODE + */ + public static final int HOT_PIXEL_MODE_HIGH_QUALITY = 2; + + // // Enumeration values for CaptureRequest#LENS_OPTICAL_STABILIZATION_MODE // /** + * <p>Optical stabilization is unavailable.</p> * @see CaptureRequest#LENS_OPTICAL_STABILIZATION_MODE */ public static final int LENS_OPTICAL_STABILIZATION_MODE_OFF = 0; /** + * <p>Optical stabilization is enabled.</p> * @see CaptureRequest#LENS_OPTICAL_STABILIZATION_MODE */ public static final int LENS_OPTICAL_STABILIZATION_MODE_ON = 1; @@ -898,32 +1296,153 @@ public abstract class CameraMetadata { // /** - * <p> - * No noise reduction is applied - * </p> + * <p>No noise reduction is applied</p> * @see CaptureRequest#NOISE_REDUCTION_MODE */ public static final int NOISE_REDUCTION_MODE_OFF = 0; /** - * <p> - * Must not slow down frame rate relative to raw - * bayer output - * </p> + * <p>Must not slow down frame rate relative to sensor + * output</p> * @see CaptureRequest#NOISE_REDUCTION_MODE */ public static final int NOISE_REDUCTION_MODE_FAST = 1; /** - * <p> - * May slow down frame rate to provide highest - * quality - * </p> + * <p>May slow down frame rate to provide highest + * quality</p> * @see CaptureRequest#NOISE_REDUCTION_MODE */ public static final int NOISE_REDUCTION_MODE_HIGH_QUALITY = 2; // + // Enumeration values for CaptureRequest#SENSOR_TEST_PATTERN_MODE + // + + /** + * <p>Default. No test pattern mode is used, and the camera + * device returns captures from the image sensor.</p> + * @see CaptureRequest#SENSOR_TEST_PATTERN_MODE + */ + public static final int SENSOR_TEST_PATTERN_MODE_OFF = 0; + + /** + * <p>Each pixel in <code>[R, G_even, G_odd, B]</code> is replaced by its + * respective color channel provided in + * {@link CaptureRequest#SENSOR_TEST_PATTERN_DATA android.sensor.testPatternData}.</p> + * <p>For example:</p> + * <pre><code>android.testPatternData = [0, 0xFFFFFFFF, 0xFFFFFFFF, 0] + * </code></pre> + * <p>All green pixels are 100% green. All red/blue pixels are black.</p> + * <pre><code>android.testPatternData = [0xFFFFFFFF, 0, 0xFFFFFFFF, 0] + * </code></pre> + * <p>All red pixels are 100% red. Only the odd green pixels + * are 100% green. All blue pixels are 100% black.</p> + * + * @see CaptureRequest#SENSOR_TEST_PATTERN_DATA + * @see CaptureRequest#SENSOR_TEST_PATTERN_MODE + */ + public static final int SENSOR_TEST_PATTERN_MODE_SOLID_COLOR = 1; + + /** + * <p>All pixel data is replaced with an 8-bar color pattern.</p> + * <p>The vertical bars (left-to-right) are as follows:</p> + * <ul> + * <li>100% white</li> + * <li>yellow</li> + * <li>cyan</li> + * <li>green</li> + * <li>magenta</li> + * <li>red</li> + * <li>blue</li> + * <li>black</li> + * </ul> + * <p>In general the image would look like the following:</p> + * <pre><code>W Y C G M R B K + * W Y C G M R B K + * W Y C G M R B K + * W Y C G M R B K + * W Y C G M R B K + * . . . . . . . . + * . . . . . . . . + * . . . . . . . . + * + * (B = Blue, K = Black) + * </code></pre> + * <p>Each bar should take up 1/8 of the sensor pixel array width. + * When this is not possible, the bar size should be rounded + * down to the nearest integer and the pattern can repeat + * on the right side.</p> + * <p>Each bar's height must always take up the full sensor + * pixel array height.</p> + * <p>Each pixel in this test pattern must be set to either + * 0% intensity or 100% intensity.</p> + * @see CaptureRequest#SENSOR_TEST_PATTERN_MODE + */ + public static final int SENSOR_TEST_PATTERN_MODE_COLOR_BARS = 2; + + /** + * <p>The test pattern is similar to COLOR_BARS, except that + * each bar should start at its specified color at the top, + * and fade to gray at the bottom.</p> + * <p>Furthermore each bar is further subdivided into a left and + * right half. The left half should have a smooth gradient, + * and the right half should have a quantized gradient.</p> + * <p>In particular, the right half's should consist of blocks of the + * same color for 1/16th active sensor pixel array width.</p> + * <p>The least significant bits in the quantized gradient should + * be copied from the most significant bits of the smooth gradient.</p> + * <p>The height of each bar should always be a multiple of 128. + * When this is not the case, the pattern should repeat at the bottom + * of the image.</p> + * @see CaptureRequest#SENSOR_TEST_PATTERN_MODE + */ + public static final int SENSOR_TEST_PATTERN_MODE_COLOR_BARS_FADE_TO_GRAY = 3; + + /** + * <p>All pixel data is replaced by a pseudo-random sequence + * generated from a PN9 512-bit sequence (typically implemented + * in hardware with a linear feedback shift register).</p> + * <p>The generator should be reset at the beginning of each frame, + * and thus each subsequent raw frame with this test pattern should + * be exactly the same as the last.</p> + * @see CaptureRequest#SENSOR_TEST_PATTERN_MODE + */ + public static final int SENSOR_TEST_PATTERN_MODE_PN9 = 4; + + /** + * <p>The first custom test pattern. All custom patterns that are + * available only on this camera device are at least this numeric + * value.</p> + * <p>All of the custom test patterns will be static + * (that is the raw image must not vary from frame to frame).</p> + * @see CaptureRequest#SENSOR_TEST_PATTERN_MODE + */ + public static final int SENSOR_TEST_PATTERN_MODE_CUSTOM1 = 256; + + // + // Enumeration values for CaptureRequest#SHADING_MODE + // + + /** + * <p>No lens shading correction is applied</p> + * @see CaptureRequest#SHADING_MODE + */ + public static final int SHADING_MODE_OFF = 0; + + /** + * <p>Must not slow down frame rate relative to sensor raw output</p> + * @see CaptureRequest#SHADING_MODE + */ + public static final int SHADING_MODE_FAST = 1; + + /** + * <p>Frame rate may be reduced by high quality</p> + * @see CaptureRequest#SHADING_MODE + */ + public static final int SHADING_MODE_HIGH_QUALITY = 2; + + // // Enumeration values for CaptureRequest#STATISTICS_FACE_DETECT_MODE // @@ -933,19 +1452,15 @@ public abstract class CameraMetadata { public static final int STATISTICS_FACE_DETECT_MODE_OFF = 0; /** - * <p> - * Optional Return rectangle and confidence - * only - * </p> + * <p>Optional Return rectangle and confidence + * only</p> * @see CaptureRequest#STATISTICS_FACE_DETECT_MODE */ public static final int STATISTICS_FACE_DETECT_MODE_SIMPLE = 1; /** - * <p> - * Optional Return all face - * metadata - * </p> + * <p>Optional Return all face + * metadata</p> * @see CaptureRequest#STATISTICS_FACE_DETECT_MODE */ public static final int STATISTICS_FACE_DETECT_MODE_FULL = 2; @@ -969,28 +1484,32 @@ public abstract class CameraMetadata { // /** - * <p> - * Use the tone mapping curve specified in - * android.tonemap.curve - * </p> + * <p>Use the tone mapping curve specified in + * the android.tonemap.curve* entries.</p> + * <p>All color enhancement and tonemapping must be disabled, except + * for applying the tonemapping curve specified by + * {@link CaptureRequest#TONEMAP_CURVE_RED android.tonemap.curveRed}, {@link CaptureRequest#TONEMAP_CURVE_BLUE android.tonemap.curveBlue}, or + * {@link CaptureRequest#TONEMAP_CURVE_GREEN android.tonemap.curveGreen}.</p> + * <p>Must not slow down frame rate relative to raw + * sensor output.</p> + * + * @see CaptureRequest#TONEMAP_CURVE_BLUE + * @see CaptureRequest#TONEMAP_CURVE_GREEN + * @see CaptureRequest#TONEMAP_CURVE_RED * @see CaptureRequest#TONEMAP_MODE */ public static final int TONEMAP_MODE_CONTRAST_CURVE = 0; /** - * <p> - * Must not slow down frame rate relative to raw - * bayer output - * </p> + * <p>Advanced gamma mapping and color enhancement may be applied.</p> + * <p>Should not slow down frame rate relative to raw sensor output.</p> * @see CaptureRequest#TONEMAP_MODE */ public static final int TONEMAP_MODE_FAST = 1; /** - * <p> - * Frame rate may be reduced by high - * quality - * </p> + * <p>Advanced gamma mapping and color enhancement may be applied.</p> + * <p>May slow down frame rate relative to raw sensor output.</p> * @see CaptureRequest#TONEMAP_MODE */ public static final int TONEMAP_MODE_HIGH_QUALITY = 2; @@ -1000,60 +1519,51 @@ public abstract class CameraMetadata { // /** - * <p> - * AE is off. When a camera device is opened, it starts in - * this state. - * </p> + * <p>AE is off or recently reset. When a camera device is opened, it starts in + * this state. This is a transient state, the camera device may skip reporting + * this state in capture result.</p> * @see CaptureResult#CONTROL_AE_STATE */ public static final int CONTROL_AE_STATE_INACTIVE = 0; /** - * <p> - * AE doesn't yet have a good set of control values - * for the current scene - * </p> + * <p>AE doesn't yet have a good set of control values + * for the current scene. This is a transient state, the camera device may skip + * reporting this state in capture result.</p> * @see CaptureResult#CONTROL_AE_STATE */ public static final int CONTROL_AE_STATE_SEARCHING = 1; /** - * <p> - * AE has a good set of control values for the - * current scene - * </p> + * <p>AE has a good set of control values for the + * current scene.</p> * @see CaptureResult#CONTROL_AE_STATE */ public static final int CONTROL_AE_STATE_CONVERGED = 2; /** - * <p> - * AE has been locked (aeMode = - * LOCKED) - * </p> + * <p>AE has been locked.</p> * @see CaptureResult#CONTROL_AE_STATE */ public static final int CONTROL_AE_STATE_LOCKED = 3; /** - * <p> - * AE has a good set of control values, but flash + * <p>AE has a good set of control values, but flash * needs to be fired for good quality still - * capture - * </p> + * capture.</p> * @see CaptureResult#CONTROL_AE_STATE */ public static final int CONTROL_AE_STATE_FLASH_REQUIRED = 4; /** - * <p> - * AE has been asked to do a precapture sequence - * (through the - * trigger_action(CAMERA2_TRIGGER_PRECAPTURE_METERING) - * call), and is currently executing it. Once PRECAPTURE + * <p>AE has been asked to do a precapture sequence + * (through the {@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER android.control.aePrecaptureTrigger} START), + * and is currently executing it. Once PRECAPTURE * completes, AE will transition to CONVERGED or - * FLASH_REQUIRED as appropriate - * </p> + * FLASH_REQUIRED as appropriate. This is a transient state, the + * camera device may skip reporting this state in capture result.</p> + * + * @see CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER * @see CaptureResult#CONTROL_AE_STATE */ public static final int CONTROL_AE_STATE_PRECAPTURE = 5; @@ -1063,71 +1573,62 @@ public abstract class CameraMetadata { // /** - * <p> - * AF off or has not yet tried to scan/been asked + * <p>AF off or has not yet tried to scan/been asked * to scan. When a camera device is opened, it starts in - * this state. - * </p> + * this state. This is a transient state, the camera device may + * skip reporting this state in capture result.</p> * @see CaptureResult#CONTROL_AF_STATE */ public static final int CONTROL_AF_STATE_INACTIVE = 0; /** - * <p> - * if CONTINUOUS_* modes are supported. AF is + * <p>if CONTINUOUS_* modes are supported. AF is * currently doing an AF scan initiated by a continuous - * autofocus mode - * </p> + * autofocus mode. This is a transient state, the camera device may + * skip reporting this state in capture result.</p> * @see CaptureResult#CONTROL_AF_STATE */ public static final int CONTROL_AF_STATE_PASSIVE_SCAN = 1; /** - * <p> - * if CONTINUOUS_* modes are supported. AF currently + * <p>if CONTINUOUS_* modes are supported. AF currently * believes it is in focus, but may restart scanning at - * any time. - * </p> + * any time. This is a transient state, the camera device may skip + * reporting this state in capture result.</p> * @see CaptureResult#CONTROL_AF_STATE */ public static final int CONTROL_AF_STATE_PASSIVE_FOCUSED = 2; /** - * <p> - * if AUTO or MACRO modes are supported. AF is doing - * an AF scan because it was triggered by AF - * trigger - * </p> + * <p>if AUTO or MACRO modes are supported. AF is doing + * an AF scan because it was triggered by AF trigger. This is a + * transient state, the camera device may skip reporting + * this state in capture result.</p> * @see CaptureResult#CONTROL_AF_STATE */ public static final int CONTROL_AF_STATE_ACTIVE_SCAN = 3; /** - * <p> - * if any AF mode besides OFF is supported. AF + * <p>if any AF mode besides OFF is supported. AF * believes it is focused correctly and is - * locked - * </p> + * locked.</p> * @see CaptureResult#CONTROL_AF_STATE */ public static final int CONTROL_AF_STATE_FOCUSED_LOCKED = 4; /** - * <p> - * if any AF mode besides OFF is supported. AF has + * <p>if any AF mode besides OFF is supported. AF has * failed to focus successfully and is - * locked - * </p> + * locked.</p> * @see CaptureResult#CONTROL_AF_STATE */ public static final int CONTROL_AF_STATE_NOT_FOCUSED_LOCKED = 5; /** - * <p> - * if CONTINUOUS_* modes are supported. AF finished a + * <p>if CONTINUOUS_* modes are supported. AF finished a * passive scan without finding focus, and may restart - * scanning at any time. - * </p> + * scanning at any time. This is a transient state, the camera + * device may skip reporting this state in capture result.</p> * @see CaptureResult#CONTROL_AF_STATE */ public static final int CONTROL_AF_STATE_PASSIVE_UNFOCUSED = 6; @@ -1137,37 +1638,30 @@ public abstract class CameraMetadata { // /** - * <p> - * AWB is not in auto mode. When a camera device is opened, it - * starts in this state. - * </p> + * <p>AWB is not in auto mode. When a camera device is opened, it + * starts in this state. This is a transient state, the camera device may + * skip reporting this state in capture result.</p> * @see CaptureResult#CONTROL_AWB_STATE */ public static final int CONTROL_AWB_STATE_INACTIVE = 0; /** - * <p> - * AWB doesn't yet have a good set of control - * values for the current scene - * </p> + * <p>AWB doesn't yet have a good set of control + * values for the current scene. This is a transient state, the camera device + * may skip reporting this state in capture result.</p> * @see CaptureResult#CONTROL_AWB_STATE */ public static final int CONTROL_AWB_STATE_SEARCHING = 1; /** - * <p> - * AWB has a good set of control values for the - * current scene - * </p> + * <p>AWB has a good set of control values for the + * current scene.</p> * @see CaptureResult#CONTROL_AWB_STATE */ public static final int CONTROL_AWB_STATE_CONVERGED = 2; /** - * <p> - * AE has been locked (aeMode = - * LOCKED) - * </p> + * <p>AWB has been locked.</p> * @see CaptureResult#CONTROL_AWB_STATE */ public static final int CONTROL_AWB_STATE_LOCKED = 3; @@ -1177,55 +1671,170 @@ public abstract class CameraMetadata { // /** - * <p> - * No flash on camera - * </p> + * <p>No flash on camera.</p> * @see CaptureResult#FLASH_STATE */ public static final int FLASH_STATE_UNAVAILABLE = 0; /** - * <p> - * if android.flash.available is true Flash is - * charging and cannot be fired - * </p> + * <p>Flash is charging and cannot be fired.</p> * @see CaptureResult#FLASH_STATE */ public static final int FLASH_STATE_CHARGING = 1; /** - * <p> - * if android.flash.available is true Flash is - * ready to fire - * </p> + * <p>Flash is ready to fire.</p> * @see CaptureResult#FLASH_STATE */ public static final int FLASH_STATE_READY = 2; /** - * <p> - * if android.flash.available is true Flash fired - * for this capture - * </p> + * <p>Flash fired for this capture.</p> * @see CaptureResult#FLASH_STATE */ public static final int FLASH_STATE_FIRED = 3; + /** + * <p>Flash partially illuminated this frame. This is usually due to the next + * or previous frame having the flash fire, and the flash spilling into this capture + * due to hardware limitations.</p> + * @see CaptureResult#FLASH_STATE + */ + public static final int FLASH_STATE_PARTIAL = 4; + // // Enumeration values for CaptureResult#LENS_STATE // /** + * <p>The lens parameters ({@link CaptureRequest#LENS_FOCAL_LENGTH android.lens.focalLength}, {@link CaptureRequest#LENS_FOCUS_DISTANCE android.lens.focusDistance}, + * {@link CaptureRequest#LENS_FILTER_DENSITY android.lens.filterDensity} and {@link CaptureRequest#LENS_APERTURE android.lens.aperture}) are not changing.</p> + * + * @see CaptureRequest#LENS_APERTURE + * @see CaptureRequest#LENS_FILTER_DENSITY + * @see CaptureRequest#LENS_FOCAL_LENGTH + * @see CaptureRequest#LENS_FOCUS_DISTANCE * @see CaptureResult#LENS_STATE */ public static final int LENS_STATE_STATIONARY = 0; /** + * <p>Any of the lens parameters ({@link CaptureRequest#LENS_FOCAL_LENGTH android.lens.focalLength}, {@link CaptureRequest#LENS_FOCUS_DISTANCE android.lens.focusDistance}, + * {@link CaptureRequest#LENS_FILTER_DENSITY android.lens.filterDensity} or {@link CaptureRequest#LENS_APERTURE android.lens.aperture}) is changing.</p> + * + * @see CaptureRequest#LENS_APERTURE + * @see CaptureRequest#LENS_FILTER_DENSITY + * @see CaptureRequest#LENS_FOCAL_LENGTH + * @see CaptureRequest#LENS_FOCUS_DISTANCE * @see CaptureResult#LENS_STATE */ public static final int LENS_STATE_MOVING = 1; // + // Enumeration values for CaptureResult#SENSOR_REFERENCE_ILLUMINANT + // + + /** + * @see CaptureResult#SENSOR_REFERENCE_ILLUMINANT + */ + public static final int SENSOR_REFERENCE_ILLUMINANT_DAYLIGHT = 1; + + /** + * @see CaptureResult#SENSOR_REFERENCE_ILLUMINANT + */ + public static final int SENSOR_REFERENCE_ILLUMINANT_FLUORESCENT = 2; + + /** + * <p>Incandescent light</p> + * @see CaptureResult#SENSOR_REFERENCE_ILLUMINANT + */ + public static final int SENSOR_REFERENCE_ILLUMINANT_TUNGSTEN = 3; + + /** + * @see CaptureResult#SENSOR_REFERENCE_ILLUMINANT + */ + public static final int SENSOR_REFERENCE_ILLUMINANT_FLASH = 4; + + /** + * @see CaptureResult#SENSOR_REFERENCE_ILLUMINANT + */ + public static final int SENSOR_REFERENCE_ILLUMINANT_FINE_WEATHER = 9; + + /** + * @see CaptureResult#SENSOR_REFERENCE_ILLUMINANT + */ + public static final int SENSOR_REFERENCE_ILLUMINANT_CLOUDY_WEATHER = 10; + + /** + * @see CaptureResult#SENSOR_REFERENCE_ILLUMINANT + */ + public static final int SENSOR_REFERENCE_ILLUMINANT_SHADE = 11; + + /** + * <p>D 5700 - 7100K</p> + * @see CaptureResult#SENSOR_REFERENCE_ILLUMINANT + */ + public static final int SENSOR_REFERENCE_ILLUMINANT_DAYLIGHT_FLUORESCENT = 12; + + /** + * <p>N 4600 - 5400K</p> + * @see CaptureResult#SENSOR_REFERENCE_ILLUMINANT + */ + public static final int SENSOR_REFERENCE_ILLUMINANT_DAY_WHITE_FLUORESCENT = 13; + + /** + * <p>W 3900 - 4500K</p> + * @see CaptureResult#SENSOR_REFERENCE_ILLUMINANT + */ + public static final int SENSOR_REFERENCE_ILLUMINANT_COOL_WHITE_FLUORESCENT = 14; + + /** + * <p>WW 3200 - 3700K</p> + * @see CaptureResult#SENSOR_REFERENCE_ILLUMINANT + */ + public static final int SENSOR_REFERENCE_ILLUMINANT_WHITE_FLUORESCENT = 15; + + /** + * @see CaptureResult#SENSOR_REFERENCE_ILLUMINANT + */ + public static final int SENSOR_REFERENCE_ILLUMINANT_STANDARD_A = 17; + + /** + * @see CaptureResult#SENSOR_REFERENCE_ILLUMINANT + */ + public static final int SENSOR_REFERENCE_ILLUMINANT_STANDARD_B = 18; + + /** + * @see CaptureResult#SENSOR_REFERENCE_ILLUMINANT + */ + public static final int SENSOR_REFERENCE_ILLUMINANT_STANDARD_C = 19; + + /** + * @see CaptureResult#SENSOR_REFERENCE_ILLUMINANT + */ + public static final int SENSOR_REFERENCE_ILLUMINANT_D55 = 20; + + /** + * @see CaptureResult#SENSOR_REFERENCE_ILLUMINANT + */ + public static final int SENSOR_REFERENCE_ILLUMINANT_D65 = 21; + + /** + * @see CaptureResult#SENSOR_REFERENCE_ILLUMINANT + */ + public static final int SENSOR_REFERENCE_ILLUMINANT_D75 = 22; + + /** + * @see CaptureResult#SENSOR_REFERENCE_ILLUMINANT + */ + public static final int SENSOR_REFERENCE_ILLUMINANT_D50 = 23; + + /** + * @see CaptureResult#SENSOR_REFERENCE_ILLUMINANT + */ + public static final int SENSOR_REFERENCE_ILLUMINANT_ISO_STUDIO_TUNGSTEN = 24; + + // // Enumeration values for CaptureResult#STATISTICS_SCENE_FLICKER // @@ -1244,6 +1853,43 @@ public abstract class CameraMetadata { */ public static final int STATISTICS_SCENE_FLICKER_60HZ = 2; + // + // Enumeration values for CaptureResult#SYNC_FRAME_NUMBER + // + + /** + * <p>The current result is not yet fully synchronized to any request. + * Synchronization is in progress, and reading metadata from this + * result may include a mix of data that have taken effect since the + * last synchronization time.</p> + * <p>In some future result, within {@link CameraCharacteristics#SYNC_MAX_LATENCY android.sync.maxLatency} frames, + * this value will update to the actual frame number frame number + * the result is guaranteed to be synchronized to (as long as the + * request settings remain constant).</p> + * + * @see CameraCharacteristics#SYNC_MAX_LATENCY + * @see CaptureResult#SYNC_FRAME_NUMBER + * @hide + */ + public static final int SYNC_FRAME_NUMBER_CONVERGING = -1; + + /** + * <p>The current result's synchronization status is unknown. The + * result may have already converged, or it may be in progress. + * Reading from this result may include some mix of settings from + * past requests.</p> + * <p>After a settings change, the new settings will eventually all + * take effect for the output buffers and results. However, this + * value will not change when that happens. Altering settings + * rapidly may provide outcomes using mixes of settings from recent + * requests.</p> + * <p>This value is intended primarily for backwards compatibility with + * the older camera implementations (for android.hardware.Camera).</p> + * @see CaptureResult#SYNC_FRAME_NUMBER + * @hide + */ + public static final int SYNC_FRAME_NUMBER_UNKNOWN = -2; + /*~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~ * End generated code *~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~O@*/ diff --git a/core/java/android/hardware/camera2/CaptureFailure.java b/core/java/android/hardware/camera2/CaptureFailure.java index 3b408cf..35f9af1 100644 --- a/core/java/android/hardware/camera2/CaptureFailure.java +++ b/core/java/android/hardware/camera2/CaptureFailure.java @@ -15,8 +15,6 @@ */ package android.hardware.camera2; -import android.hardware.camera2.CameraDevice.CaptureListener; - /** * A report of failed capture for a single image capture from the image sensor. * diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java index 898f123..c4e342c 100644 --- a/core/java/android/hardware/camera2/CaptureRequest.java +++ b/core/java/android/hardware/camera2/CaptureRequest.java @@ -17,7 +17,6 @@ package android.hardware.camera2; import android.hardware.camera2.impl.CameraMetadataNative; -import android.hardware.camera2.CameraDevice.CaptureListener; import android.os.Parcel; import android.os.Parcelable; import android.view.Surface; @@ -318,11 +317,52 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { * modify the comment blocks at the start or end. *~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~*/ + /** - * <p> - * When android.control.awbMode is not OFF, TRANSFORM_MATRIX - * should be ignored. - * </p> + * <p>The mode control selects how the image data is converted from the + * sensor's native color into linear sRGB color.</p> + * <p>When auto-white balance is enabled with {@link CaptureRequest#CONTROL_AWB_MODE android.control.awbMode}, this + * control is overridden by the AWB routine. When AWB is disabled, the + * application controls how the color mapping is performed.</p> + * <p>We define the expected processing pipeline below. For consistency + * across devices, this is always the case with TRANSFORM_MATRIX.</p> + * <p>When either FULL or HIGH_QUALITY is used, the camera device may + * do additional processing but {@link CaptureRequest#COLOR_CORRECTION_GAINS android.colorCorrection.gains} and + * {@link CaptureRequest#COLOR_CORRECTION_TRANSFORM android.colorCorrection.transform} will still be provided by the + * camera device (in the results) and be roughly correct.</p> + * <p>Switching to TRANSFORM_MATRIX and using the data provided from + * FAST or HIGH_QUALITY will yield a picture with the same white point + * as what was produced by the camera device in the earlier frame.</p> + * <p>The expected processing pipeline is as follows:</p> + * <p><img alt="White balance processing pipeline" src="../../../../images/camera2/metadata/android.colorCorrection.mode/processing_pipeline.png" /></p> + * <p>The white balance is encoded by two values, a 4-channel white-balance + * gain vector (applied in the Bayer domain), and a 3x3 color transform + * matrix (applied after demosaic).</p> + * <p>The 4-channel white-balance gains are defined as:</p> + * <pre><code>{@link CaptureRequest#COLOR_CORRECTION_GAINS android.colorCorrection.gains} = [ R G_even G_odd B ] + * </code></pre> + * <p>where <code>G_even</code> is the gain for green pixels on even rows of the + * output, and <code>G_odd</code> is the gain for green pixels on the odd rows. + * These may be identical for a given camera device implementation; if + * the camera device does not support a separate gain for even/odd green + * channels, it will use the <code>G_even</code> value, and write <code>G_odd</code> equal to + * <code>G_even</code> in the output result metadata.</p> + * <p>The matrices for color transforms are defined as a 9-entry vector:</p> + * <pre><code>{@link CaptureRequest#COLOR_CORRECTION_TRANSFORM android.colorCorrection.transform} = [ I0 I1 I2 I3 I4 I5 I6 I7 I8 ] + * </code></pre> + * <p>which define a transform from input sensor colors, <code>P_in = [ r g b ]</code>, + * to output linear sRGB, <code>P_out = [ r' g' b' ]</code>,</p> + * <p>with colors as follows:</p> + * <pre><code>r' = I0r + I1g + I2b + * g' = I3r + I4g + I5b + * b' = I6r + I7g + I8b + * </code></pre> + * <p>Both the input and output value ranges must match. Overflow/underflow + * values are clipped to fit within the range.</p> + * + * @see CaptureRequest#COLOR_CORRECTION_GAINS + * @see CaptureRequest#COLOR_CORRECTION_TRANSFORM + * @see CaptureRequest#CONTROL_AWB_MODE * @see #COLOR_CORRECTION_MODE_TRANSFORM_MATRIX * @see #COLOR_CORRECTION_MODE_FAST * @see #COLOR_CORRECTION_MODE_HIGH_QUALITY @@ -331,55 +371,80 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { new Key<Integer>("android.colorCorrection.mode", int.class); /** - * <p> - * A color transform matrix to use to transform - * from sensor RGB color space to output linear sRGB color space - * </p> - * <p> - * This matrix is either set by HAL when the request - * android.colorCorrection.mode is not TRANSFORM_MATRIX, or + * <p>A color transform matrix to use to transform + * from sensor RGB color space to output linear sRGB color space</p> + * <p>This matrix is either set by the camera device when the request + * {@link CaptureRequest#COLOR_CORRECTION_MODE android.colorCorrection.mode} is not TRANSFORM_MATRIX, or * directly by the application in the request when the - * android.colorCorrection.mode is TRANSFORM_MATRIX. - * </p><p> - * In the latter case, the HAL may round the matrix to account - * for precision issues; the final rounded matrix should be - * reported back in this matrix result metadata. - * </p> + * {@link CaptureRequest#COLOR_CORRECTION_MODE android.colorCorrection.mode} is TRANSFORM_MATRIX.</p> + * <p>In the latter case, the camera device may round the matrix to account + * for precision issues; the final rounded matrix should be reported back + * in this matrix result metadata. The transform should keep the magnitude + * of the output color values within <code>[0, 1.0]</code> (assuming input color + * values is within the normalized range <code>[0, 1.0]</code>), or clipping may occur.</p> + * + * @see CaptureRequest#COLOR_CORRECTION_MODE */ public static final Key<Rational[]> COLOR_CORRECTION_TRANSFORM = new Key<Rational[]>("android.colorCorrection.transform", Rational[].class); /** - * <p> - * Gains applying to Bayer color channels for - * white-balance - * </p> - * <p> - * The 4-channel white-balance gains are defined in - * the order of [R G_even G_odd B], where G_even is the gain - * for green pixels on even rows of the output, and G_odd - * is the gain for greenpixels on the odd rows. if a HAL + * <p>Gains applying to Bayer raw color channels for + * white-balance.</p> + * <p>The 4-channel white-balance gains are defined in + * the order of <code>[R G_even G_odd B]</code>, where <code>G_even</code> is the gain + * for green pixels on even rows of the output, and <code>G_odd</code> + * is the gain for green pixels on the odd rows. if a HAL * does not support a separate gain for even/odd green channels, - * it should use the G_even value,and write G_odd equal to - * G_even in the output result metadata. - * </p><p> - * This array is either set by HAL when the request - * android.colorCorrection.mode is not TRANSFORM_MATRIX, or + * it should use the <code>G_even</code> value, and write <code>G_odd</code> equal to + * <code>G_even</code> in the output result metadata.</p> + * <p>This array is either set by the camera device when the request + * {@link CaptureRequest#COLOR_CORRECTION_MODE android.colorCorrection.mode} is not TRANSFORM_MATRIX, or * directly by the application in the request when the - * android.colorCorrection.mode is TRANSFORM_MATRIX. - * </p><p> - * The ouput should be the gains actually applied by the HAL to - * the current frame. - * </p> + * {@link CaptureRequest#COLOR_CORRECTION_MODE android.colorCorrection.mode} is TRANSFORM_MATRIX.</p> + * <p>The output should be the gains actually applied by the camera device to + * the current frame.</p> + * + * @see CaptureRequest#COLOR_CORRECTION_MODE */ public static final Key<float[]> COLOR_CORRECTION_GAINS = new Key<float[]>("android.colorCorrection.gains", float[].class); /** - * <p> - * Enum for controlling - * antibanding - * </p> + * <p>The desired setting for the camera device's auto-exposure + * algorithm's antibanding compensation.</p> + * <p>Some kinds of lighting fixtures, such as some fluorescent + * lights, flicker at the rate of the power supply frequency + * (60Hz or 50Hz, depending on country). While this is + * typically not noticeable to a person, it can be visible to + * a camera device. If a camera sets its exposure time to the + * wrong value, the flicker may become visible in the + * viewfinder as flicker or in a final captured image, as a + * set of variable-brightness bands across the image.</p> + * <p>Therefore, the auto-exposure routines of camera devices + * include antibanding routines that ensure that the chosen + * exposure value will not cause such banding. The choice of + * exposure time depends on the rate of flicker, which the + * camera device can detect automatically, or the expected + * rate can be selected by the application using this + * control.</p> + * <p>A given camera device may not support all of the possible + * options for the antibanding mode. The + * {@link CameraCharacteristics#CONTROL_AE_AVAILABLE_ANTIBANDING_MODES android.control.aeAvailableAntibandingModes} key contains + * the available modes for a given camera device.</p> + * <p>The default mode is AUTO, which must be supported by all + * camera devices.</p> + * <p>If manual exposure control is enabled (by setting + * {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} or {@link CaptureRequest#CONTROL_MODE android.control.mode} to OFF), + * then this setting has no effect, and the application must + * ensure it selects exposure times that do not cause banding + * issues. The {@link CaptureResult#STATISTICS_SCENE_FLICKER android.statistics.sceneFlicker} key can assist + * the application in this.</p> + * + * @see CameraCharacteristics#CONTROL_AE_AVAILABLE_ANTIBANDING_MODES + * @see CaptureRequest#CONTROL_AE_MODE + * @see CaptureRequest#CONTROL_MODE + * @see CaptureResult#STATISTICS_SCENE_FLICKER * @see #CONTROL_AE_ANTIBANDING_MODE_OFF * @see #CONTROL_AE_ANTIBANDING_MODE_50HZ * @see #CONTROL_AE_ANTIBANDING_MODE_60HZ @@ -389,42 +454,66 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { new Key<Integer>("android.control.aeAntibandingMode", int.class); /** - * <p> - * Adjustment to AE target image - * brightness - * </p> - * <p> - * For example, if EV step is 0.333, '6' will mean an + * <p>Adjustment to AE target image + * brightness</p> + * <p>For example, if EV step is 0.333, '6' will mean an * exposure compensation of +2 EV; -3 will mean an exposure - * compensation of -1 - * </p> + * compensation of -1</p> */ public static final Key<Integer> CONTROL_AE_EXPOSURE_COMPENSATION = new Key<Integer>("android.control.aeExposureCompensation", int.class); /** - * <p> - * Whether AE is currently locked to its latest - * calculated values - * </p> - * <p> - * Note that even when AE is locked, the flash may be - * fired if the AE mode is ON_AUTO_FLASH / ON_ALWAYS_FLASH / - * ON_AUTO_FLASH_REDEYE. - * </p> + * <p>Whether AE is currently locked to its latest + * calculated values.</p> + * <p>Note that even when AE is locked, the flash may be + * fired if the {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} is ON_AUTO_FLASH / ON_ALWAYS_FLASH / + * ON_AUTO_FLASH_REDEYE.</p> + * <p>If AE precapture is triggered (see {@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER android.control.aePrecaptureTrigger}) + * when AE is already locked, the camera device will not change the exposure time + * ({@link CaptureRequest#SENSOR_EXPOSURE_TIME android.sensor.exposureTime}) and sensitivity ({@link CaptureRequest#SENSOR_SENSITIVITY android.sensor.sensitivity}) + * parameters. The flash may be fired if the {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} + * is ON_AUTO_FLASH/ON_AUTO_FLASH_REDEYE and the scene is too dark. If the + * {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} is ON_ALWAYS_FLASH, the scene may become overexposed.</p> + * <p>See {@link CaptureResult#CONTROL_AE_STATE android.control.aeState} for AE lock related state transition details.</p> + * + * @see CaptureRequest#CONTROL_AE_MODE + * @see CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER + * @see CaptureResult#CONTROL_AE_STATE + * @see CaptureRequest#SENSOR_EXPOSURE_TIME + * @see CaptureRequest#SENSOR_SENSITIVITY */ public static final Key<Boolean> CONTROL_AE_LOCK = new Key<Boolean>("android.control.aeLock", boolean.class); /** - * <p> - * Whether AE is currently updating the sensor - * exposure and sensitivity fields - * </p> - * <p> - * Only effective if android.control.mode = - * AUTO - * </p> + * <p>The desired mode for the camera device's + * auto-exposure routine.</p> + * <p>This control is only effective if {@link CaptureRequest#CONTROL_MODE android.control.mode} is + * AUTO.</p> + * <p>When set to any of the ON modes, the camera device's + * auto-exposure routine is enabled, overriding the + * application's selected exposure time, sensor sensitivity, + * and frame duration ({@link CaptureRequest#SENSOR_EXPOSURE_TIME android.sensor.exposureTime}, + * {@link CaptureRequest#SENSOR_SENSITIVITY android.sensor.sensitivity}, and + * {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration}). If one of the FLASH modes + * is selected, the camera device's flash unit controls are + * also overridden.</p> + * <p>The FLASH modes are only available if the camera device + * has a flash unit ({@link CameraCharacteristics#FLASH_INFO_AVAILABLE android.flash.info.available} is <code>true</code>).</p> + * <p>If flash TORCH mode is desired, this field must be set to + * ON or OFF, and {@link CaptureRequest#FLASH_MODE android.flash.mode} set to TORCH.</p> + * <p>When set to any of the ON modes, the values chosen by the + * camera device auto-exposure routine for the overridden + * fields for a given capture will be available in its + * CaptureResult.</p> + * + * @see CaptureRequest#CONTROL_MODE + * @see CameraCharacteristics#FLASH_INFO_AVAILABLE + * @see CaptureRequest#FLASH_MODE + * @see CaptureRequest#SENSOR_EXPOSURE_TIME + * @see CaptureRequest#SENSOR_FRAME_DURATION + * @see CaptureRequest#SENSOR_SENSITIVITY * @see #CONTROL_AE_MODE_OFF * @see #CONTROL_AE_MODE_ON * @see #CONTROL_AE_MODE_ON_AUTO_FLASH @@ -435,60 +524,52 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { new Key<Integer>("android.control.aeMode", int.class); /** - * <p> - * List of areas to use for - * metering - * </p> - * <p> - * Each area is a rectangle plus weight: xmin, ymin, - * xmax, ymax, weight. The rectangle is defined inclusive of the - * specified coordinates. - * </p><p> - * The coordinate system is based on the active pixel array, + * <p>List of areas to use for + * metering.</p> + * <p>Each area is a rectangle plus weight: xmin, ymin, + * xmax, ymax, weight. The rectangle is defined to be inclusive of the + * specified coordinates.</p> + * <p>The coordinate system is based on the active pixel array, * with (0,0) being the top-left pixel in the active pixel array, and - * (android.sensor.info.activeArraySize.width - 1, - * android.sensor.info.activeArraySize.height - 1) being the + * ({@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE android.sensor.info.activeArraySize}.width - 1, + * {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE android.sensor.info.activeArraySize}.height - 1) being the * bottom-right pixel in the active pixel array. The weight - * should be nonnegative. - * </p><p> - * If all regions have 0 weight, then no specific metering area - * needs to be used by the HAL. If the metering region is - * outside the current android.scaler.cropRegion, the HAL - * should ignore the sections outside the region and output the - * used sections in the frame metadata - * </p> + * should be nonnegative.</p> + * <p>If all regions have 0 weight, then no specific metering area + * needs to be used by the camera device. If the metering region is + * outside the current {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion}, the camera device + * will ignore the sections outside the region and output the + * used sections in the frame metadata.</p> + * + * @see CaptureRequest#SCALER_CROP_REGION + * @see CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE */ public static final Key<int[]> CONTROL_AE_REGIONS = new Key<int[]>("android.control.aeRegions", int[].class); /** - * <p> - * Range over which fps can be adjusted to - * maintain exposure - * </p> - * <p> - * Only constrains AE algorithm, not manual control - * of android.sensor.exposureTime - * </p> + * <p>Range over which fps can be adjusted to + * maintain exposure</p> + * <p>Only constrains AE algorithm, not manual control + * of {@link CaptureRequest#SENSOR_EXPOSURE_TIME android.sensor.exposureTime}</p> + * + * @see CaptureRequest#SENSOR_EXPOSURE_TIME */ public static final Key<int[]> CONTROL_AE_TARGET_FPS_RANGE = new Key<int[]>("android.control.aeTargetFpsRange", int[].class); /** - * <p> - * Whether the HAL must trigger precapture - * metering. - * </p> - * <p> - * This entry is normally set to IDLE, or is not + * <p>Whether the camera device will trigger a precapture + * metering sequence when it processes this request.</p> + * <p>This entry is normally set to IDLE, or is not * included at all in the request settings. When included and - * set to START, the HAL must trigger the autoexposure - * precapture metering sequence. - * </p><p> - * The effect of AE precapture trigger depends on the current - * AE mode and state; see the camera HAL device v3 header for - * details. - * </p> + * set to START, the camera device will trigger the autoexposure + * precapture metering sequence.</p> + * <p>The effect of AE precapture trigger depends on the current + * AE mode and state; see {@link CaptureResult#CONTROL_AE_STATE android.control.aeState} for AE precapture + * state transition details.</p> + * + * @see CaptureResult#CONTROL_AE_STATE * @see #CONTROL_AE_PRECAPTURE_TRIGGER_IDLE * @see #CONTROL_AE_PRECAPTURE_TRIGGER_START */ @@ -496,10 +577,17 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { new Key<Integer>("android.control.aePrecaptureTrigger", int.class); /** - * <p> - * Whether AF is currently enabled, and what - * mode it is set to - * </p> + * <p>Whether AF is currently enabled, and what + * mode it is set to</p> + * <p>Only effective if {@link CaptureRequest#CONTROL_MODE android.control.mode} = AUTO and the lens is not fixed focus + * (i.e. <code>{@link CameraCharacteristics#LENS_INFO_MINIMUM_FOCUS_DISTANCE android.lens.info.minimumFocusDistance} > 0</code>).</p> + * <p>If the lens is controlled by the camera device auto-focus algorithm, + * the camera device will report the current AF status in {@link CaptureResult#CONTROL_AF_STATE android.control.afState} + * in result metadata.</p> + * + * @see CaptureResult#CONTROL_AF_STATE + * @see CaptureRequest#CONTROL_MODE + * @see CameraCharacteristics#LENS_INFO_MINIMUM_FOCUS_DISTANCE * @see #CONTROL_AF_MODE_OFF * @see #CONTROL_AF_MODE_AUTO * @see #CONTROL_AF_MODE_MACRO @@ -511,46 +599,40 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { new Key<Integer>("android.control.afMode", int.class); /** - * <p> - * List of areas to use for focus - * estimation - * </p> - * <p> - * Each area is a rectangle plus weight: xmin, ymin, - * xmax, ymax, weight. The rectangle is defined inclusive of the - * specified coordinates. - * </p><p> - * The coordinate system is based on the active pixel array, + * <p>List of areas to use for focus + * estimation.</p> + * <p>Each area is a rectangle plus weight: xmin, ymin, + * xmax, ymax, weight. The rectangle is defined to be inclusive of the + * specified coordinates.</p> + * <p>The coordinate system is based on the active pixel array, * with (0,0) being the top-left pixel in the active pixel array, and - * (android.sensor.info.activeArraySize.width - 1, - * android.sensor.info.activeArraySize.height - 1) being the + * ({@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE android.sensor.info.activeArraySize}.width - 1, + * {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE android.sensor.info.activeArraySize}.height - 1) being the * bottom-right pixel in the active pixel array. The weight - * should be nonnegative. - * </p><p> - * If all regions have 0 weight, then no specific focus area - * needs to be used by the HAL. If the focusing region is - * outside the current android.scaler.cropRegion, the HAL - * should ignore the sections outside the region and output the - * used sections in the frame metadata - * </p> + * should be nonnegative.</p> + * <p>If all regions have 0 weight, then no specific focus area + * needs to be used by the camera device. If the focusing region is + * outside the current {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion}, the camera device + * will ignore the sections outside the region and output the + * used sections in the frame metadata.</p> + * + * @see CaptureRequest#SCALER_CROP_REGION + * @see CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE */ public static final Key<int[]> CONTROL_AF_REGIONS = new Key<int[]>("android.control.afRegions", int[].class); /** - * <p> - * Whether the HAL must trigger autofocus. - * </p> - * <p> - * This entry is normally set to IDLE, or is not - * included at all in the request settings. - * </p><p> - * When included and set to START, the HAL must trigger the - * autofocus algorithm. The effect of AF trigger depends on the - * current AF mode and state; see the camera HAL device v3 - * header for details. When set to CANCEL, the HAL must cancel - * any active trigger, and return to initial AF state. - * </p> + * <p>Whether the camera device will trigger autofocus for this request.</p> + * <p>This entry is normally set to IDLE, or is not + * included at all in the request settings.</p> + * <p>When included and set to START, the camera device will trigger the + * autofocus algorithm. If autofocus is disabled, this trigger has no effect.</p> + * <p>When set to CANCEL, the camera device will cancel any active trigger, + * and return to its initial AF state.</p> + * <p>See {@link CaptureResult#CONTROL_AF_STATE android.control.afState} for what that means for each AF mode.</p> + * + * @see CaptureResult#CONTROL_AF_STATE * @see #CONTROL_AF_TRIGGER_IDLE * @see #CONTROL_AF_TRIGGER_START * @see #CONTROL_AF_TRIGGER_CANCEL @@ -559,28 +641,36 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { new Key<Integer>("android.control.afTrigger", int.class); /** - * <p> - * Whether AWB is currently locked to its - * latest calculated values - * </p> - * <p> - * Note that AWB lock is only meaningful for AUTO + * <p>Whether AWB is currently locked to its + * latest calculated values.</p> + * <p>Note that AWB lock is only meaningful for AUTO * mode; in other modes, AWB is already fixed to a specific - * setting - * </p> + * setting.</p> */ public static final Key<Boolean> CONTROL_AWB_LOCK = new Key<Boolean>("android.control.awbLock", boolean.class); /** - * <p> - * Whether AWB is currently setting the color + * <p>Whether AWB is currently setting the color * transform fields, and what its illumination target - * is - * </p> - * <p> - * [BC - AWB lock,AWB modes] - * </p> + * is.</p> + * <p>This control is only effective if {@link CaptureRequest#CONTROL_MODE android.control.mode} is AUTO.</p> + * <p>When set to the ON mode, the camera device's auto white balance + * routine is enabled, overriding the application's selected + * {@link CaptureRequest#COLOR_CORRECTION_TRANSFORM android.colorCorrection.transform}, {@link CaptureRequest#COLOR_CORRECTION_GAINS android.colorCorrection.gains} and + * {@link CaptureRequest#COLOR_CORRECTION_MODE android.colorCorrection.mode}.</p> + * <p>When set to the OFF mode, the camera device's auto white balance + * routine is disabled. The application manually controls the white + * balance by {@link CaptureRequest#COLOR_CORRECTION_TRANSFORM android.colorCorrection.transform}, {@link CaptureRequest#COLOR_CORRECTION_GAINS android.colorCorrection.gains} + * and {@link CaptureRequest#COLOR_CORRECTION_MODE android.colorCorrection.mode}.</p> + * <p>When set to any other modes, the camera device's auto white balance + * routine is disabled. The camera device uses each particular illumination + * target for white balance adjustment.</p> + * + * @see CaptureRequest#COLOR_CORRECTION_GAINS + * @see CaptureRequest#COLOR_CORRECTION_MODE + * @see CaptureRequest#COLOR_CORRECTION_TRANSFORM + * @see CaptureRequest#CONTROL_MODE * @see #CONTROL_AWB_MODE_OFF * @see #CONTROL_AWB_MODE_AUTO * @see #CONTROL_AWB_MODE_INCANDESCENT @@ -595,58 +685,66 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { new Key<Integer>("android.control.awbMode", int.class); /** - * <p> - * List of areas to use for illuminant - * estimation - * </p> - * <p> - * Only used in AUTO mode. - * </p><p> - * Each area is a rectangle plus weight: xmin, ymin, - * xmax, ymax, weight. The rectangle is defined inclusive of the - * specified coordinates. - * </p><p> - * The coordinate system is based on the active pixel array, + * <p>List of areas to use for illuminant + * estimation.</p> + * <p>Only used in AUTO mode.</p> + * <p>Each area is a rectangle plus weight: xmin, ymin, + * xmax, ymax, weight. The rectangle is defined to be inclusive of the + * specified coordinates.</p> + * <p>The coordinate system is based on the active pixel array, * with (0,0) being the top-left pixel in the active pixel array, and - * (android.sensor.info.activeArraySize.width - 1, - * android.sensor.info.activeArraySize.height - 1) being the + * ({@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE android.sensor.info.activeArraySize}.width - 1, + * {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE android.sensor.info.activeArraySize}.height - 1) being the * bottom-right pixel in the active pixel array. The weight - * should be nonnegative. - * </p><p> - * If all regions have 0 weight, then no specific metering area - * needs to be used by the HAL. If the metering region is - * outside the current android.scaler.cropRegion, the HAL - * should ignore the sections outside the region and output the - * used sections in the frame metadata - * </p> + * should be nonnegative.</p> + * <p>If all regions have 0 weight, then no specific auto-white balance (AWB) area + * needs to be used by the camera device. If the AWB region is + * outside the current {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion}, the camera device + * will ignore the sections outside the region and output the + * used sections in the frame metadata.</p> + * + * @see CaptureRequest#SCALER_CROP_REGION + * @see CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE */ public static final Key<int[]> CONTROL_AWB_REGIONS = new Key<int[]>("android.control.awbRegions", int[].class); /** - * <p> - * Information to 3A routines about the purpose - * of this capture, to help decide optimal 3A - * strategy - * </p> - * <p> - * Only used if android.control.mode != OFF. - * </p> + * <p>Information to the camera device 3A (auto-exposure, + * auto-focus, auto-white balance) routines about the purpose + * of this capture, to help the camera device to decide optimal 3A + * strategy.</p> + * <p>This control (except for MANUAL) is only effective if + * <code>{@link CaptureRequest#CONTROL_MODE android.control.mode} != OFF</code> and any 3A routine is active.</p> + * <p>ZERO_SHUTTER_LAG must be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} + * contains ZSL. MANUAL must be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} + * contains MANUAL_SENSOR.</p> + * + * @see CaptureRequest#CONTROL_MODE + * @see CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES * @see #CONTROL_CAPTURE_INTENT_CUSTOM * @see #CONTROL_CAPTURE_INTENT_PREVIEW * @see #CONTROL_CAPTURE_INTENT_STILL_CAPTURE * @see #CONTROL_CAPTURE_INTENT_VIDEO_RECORD * @see #CONTROL_CAPTURE_INTENT_VIDEO_SNAPSHOT * @see #CONTROL_CAPTURE_INTENT_ZERO_SHUTTER_LAG + * @see #CONTROL_CAPTURE_INTENT_MANUAL */ public static final Key<Integer> CONTROL_CAPTURE_INTENT = new Key<Integer>("android.control.captureIntent", int.class); /** - * <p> - * Whether any special color effect is in use. - * Only used if android.control.mode != OFF - * </p> + * <p>A special color effect to apply.</p> + * <p>When this mode is set, a color effect will be applied + * to images produced by the camera device. The interpretation + * and implementation of these color effects is left to the + * implementor of the camera device, and should not be + * depended on to be consistent (or present) across all + * devices.</p> + * <p>A color effect will only be applied if + * {@link CaptureRequest#CONTROL_MODE android.control.mode} != OFF.</p> + * + * @see CaptureRequest#CONTROL_MODE * @see #CONTROL_EFFECT_MODE_OFF * @see #CONTROL_EFFECT_MODE_MONO * @see #CONTROL_EFFECT_MODE_NEGATIVE @@ -661,23 +759,50 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { new Key<Integer>("android.control.effectMode", int.class); /** - * <p> - * Overall mode of 3A control - * routines - * </p> + * <p>Overall mode of 3A control + * routines.</p> + * <p>High-level 3A control. When set to OFF, all 3A control + * by the camera device is disabled. The application must set the fields for + * capture parameters itself.</p> + * <p>When set to AUTO, the individual algorithm controls in + * android.control.* are in effect, such as {@link CaptureRequest#CONTROL_AF_MODE android.control.afMode}.</p> + * <p>When set to USE_SCENE_MODE, the individual controls in + * android.control.* are mostly disabled, and the camera device implements + * one of the scene mode settings (such as ACTION, SUNSET, or PARTY) + * as it wishes. The camera device scene mode 3A settings are provided by + * android.control.sceneModeOverrides.</p> + * <p>When set to OFF_KEEP_STATE, it is similar to OFF mode, the only difference + * is that this frame will not be used by camera device background 3A statistics + * update, as if this frame is never captured. This mode can be used in the scenario + * where the application doesn't want a 3A manual control capture to affect + * the subsequent auto 3A capture results.</p> + * + * @see CaptureRequest#CONTROL_AF_MODE * @see #CONTROL_MODE_OFF * @see #CONTROL_MODE_AUTO * @see #CONTROL_MODE_USE_SCENE_MODE + * @see #CONTROL_MODE_OFF_KEEP_STATE */ public static final Key<Integer> CONTROL_MODE = new Key<Integer>("android.control.mode", int.class); /** - * <p> - * Which scene mode is active when - * android.control.mode = SCENE_MODE - * </p> - * @see #CONTROL_SCENE_MODE_UNSUPPORTED + * <p>A camera mode optimized for conditions typical in a particular + * capture setting.</p> + * <p>This is the mode that that is active when + * <code>{@link CaptureRequest#CONTROL_MODE android.control.mode} == USE_SCENE_MODE</code>. Aside from FACE_PRIORITY, + * these modes will disable {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode}, + * {@link CaptureRequest#CONTROL_AWB_MODE android.control.awbMode}, and {@link CaptureRequest#CONTROL_AF_MODE android.control.afMode} while in use.</p> + * <p>The interpretation and implementation of these scene modes is left + * to the implementor of the camera device. Their behavior will not be + * consistent across all devices, and any given device may only implement + * a subset of these modes.</p> + * + * @see CaptureRequest#CONTROL_AE_MODE + * @see CaptureRequest#CONTROL_AF_MODE + * @see CaptureRequest#CONTROL_AWB_MODE + * @see CaptureRequest#CONTROL_MODE + * @see #CONTROL_SCENE_MODE_DISABLED * @see #CONTROL_SCENE_MODE_FACE_PRIORITY * @see #CONTROL_SCENE_MODE_ACTION * @see #CONTROL_SCENE_MODE_PORTRAIT @@ -699,24 +824,30 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { new Key<Integer>("android.control.sceneMode", int.class); /** - * <p> - * Whether video stabilization is - * active - * </p> - * <p> - * If enabled, video stabilization can modify the - * android.scaler.cropRegion to keep the video stream - * stabilized - * </p> + * <p>Whether video stabilization is + * active</p> + * <p>If enabled, video stabilization can modify the + * {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} to keep the video stream + * stabilized</p> + * + * @see CaptureRequest#SCALER_CROP_REGION */ public static final Key<Boolean> CONTROL_VIDEO_STABILIZATION_MODE = new Key<Boolean>("android.control.videoStabilizationMode", boolean.class); /** - * <p> - * Operation mode for edge - * enhancement - * </p> + * <p>Operation mode for edge + * enhancement.</p> + * <p>Edge/sharpness/detail enhancement. OFF means no + * enhancement will be applied by the camera device.</p> + * <p>This must be set to one of the modes listed in {@link CameraCharacteristics#EDGE_AVAILABLE_EDGE_MODES android.edge.availableEdgeModes}.</p> + * <p>FAST/HIGH_QUALITY both mean camera device determined enhancement + * will be applied. HIGH_QUALITY mode indicates that the + * camera device will use the highest-quality enhancement algorithms, + * even if it slows down capture rate. FAST means the camera device will + * not slow down capture rate when applying edge enhancement.</p> + * + * @see CameraCharacteristics#EDGE_AVAILABLE_EDGE_MODES * @see #EDGE_MODE_OFF * @see #EDGE_MODE_FAST * @see #EDGE_MODE_HIGH_QUALITY @@ -725,9 +856,25 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { new Key<Integer>("android.edge.mode", int.class); /** - * <p> - * Select flash operation mode - * </p> + * <p>The desired mode for for the camera device's flash control.</p> + * <p>This control is only effective when flash unit is available + * (<code>{@link CameraCharacteristics#FLASH_INFO_AVAILABLE android.flash.info.available} == true</code>).</p> + * <p>When this control is used, the {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} must be set to ON or OFF. + * Otherwise, the camera device auto-exposure related flash control (ON_AUTO_FLASH, + * ON_ALWAYS_FLASH, or ON_AUTO_FLASH_REDEYE) will override this control.</p> + * <p>When set to OFF, the camera device will not fire flash for this capture.</p> + * <p>When set to SINGLE, the camera device will fire flash regardless of the camera + * device's auto-exposure routine's result. When used in still capture case, this + * control should be used along with AE precapture metering sequence + * ({@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER android.control.aePrecaptureTrigger}), otherwise, the image may be incorrectly exposed.</p> + * <p>When set to TORCH, the flash will be on continuously. This mode can be used + * for use cases such as preview, auto-focus assist, still capture, or video recording.</p> + * <p>The flash status will be reported by {@link CaptureResult#FLASH_STATE android.flash.state} in the capture result metadata.</p> + * + * @see CaptureRequest#CONTROL_AE_MODE + * @see CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER + * @see CameraCharacteristics#FLASH_INFO_AVAILABLE + * @see CaptureResult#FLASH_STATE * @see #FLASH_MODE_OFF * @see #FLASH_MODE_SINGLE * @see #FLASH_MODE_TORCH @@ -736,128 +883,171 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { new Key<Integer>("android.flash.mode", int.class); /** - * <p> - * GPS coordinates to include in output JPEG - * EXIF - * </p> + * <p>Set operational mode for hot pixel correction.</p> + * <p>Valid modes for this camera device are listed in + * {@link CameraCharacteristics#HOT_PIXEL_AVAILABLE_HOT_PIXEL_MODES android.hotPixel.availableHotPixelModes}.</p> + * <p>Hotpixel correction interpolates out, or otherwise removes, pixels + * that do not accurately encode the incoming light (i.e. pixels that + * are stuck at an arbitrary value).</p> + * + * @see CameraCharacteristics#HOT_PIXEL_AVAILABLE_HOT_PIXEL_MODES + * @see #HOT_PIXEL_MODE_OFF + * @see #HOT_PIXEL_MODE_FAST + * @see #HOT_PIXEL_MODE_HIGH_QUALITY + */ + public static final Key<Integer> HOT_PIXEL_MODE = + new Key<Integer>("android.hotPixel.mode", int.class); + + /** + * <p>GPS coordinates to include in output JPEG + * EXIF</p> */ public static final Key<double[]> JPEG_GPS_COORDINATES = new Key<double[]>("android.jpeg.gpsCoordinates", double[].class); /** - * <p> - * 32 characters describing GPS algorithm to - * include in EXIF - * </p> + * <p>32 characters describing GPS algorithm to + * include in EXIF</p> */ public static final Key<String> JPEG_GPS_PROCESSING_METHOD = new Key<String>("android.jpeg.gpsProcessingMethod", String.class); /** - * <p> - * Time GPS fix was made to include in - * EXIF - * </p> + * <p>Time GPS fix was made to include in + * EXIF</p> */ public static final Key<Long> JPEG_GPS_TIMESTAMP = new Key<Long>("android.jpeg.gpsTimestamp", long.class); /** - * <p> - * Orientation of JPEG image to - * write - * </p> + * <p>Orientation of JPEG image to + * write</p> */ public static final Key<Integer> JPEG_ORIENTATION = new Key<Integer>("android.jpeg.orientation", int.class); /** - * <p> - * Compression quality of the final JPEG - * image - * </p> - * <p> - * 85-95 is typical usage range - * </p> + * <p>Compression quality of the final JPEG + * image</p> + * <p>85-95 is typical usage range</p> */ public static final Key<Byte> JPEG_QUALITY = new Key<Byte>("android.jpeg.quality", byte.class); /** - * <p> - * Compression quality of JPEG - * thumbnail - * </p> + * <p>Compression quality of JPEG + * thumbnail</p> */ public static final Key<Byte> JPEG_THUMBNAIL_QUALITY = new Key<Byte>("android.jpeg.thumbnailQuality", byte.class); /** - * <p> - * Resolution of embedded JPEG - * thumbnail - * </p> + * <p>Resolution of embedded JPEG thumbnail</p> + * <p>When set to (0, 0) value, the JPEG EXIF will not contain thumbnail, + * but the captured JPEG will still be a valid image.</p> + * <p>When a jpeg image capture is issued, the thumbnail size selected should have + * the same aspect ratio as the jpeg image.</p> */ public static final Key<android.hardware.camera2.Size> JPEG_THUMBNAIL_SIZE = new Key<android.hardware.camera2.Size>("android.jpeg.thumbnailSize", android.hardware.camera2.Size.class); /** - * <p> - * Size of the lens aperture - * </p> - * <p> - * Will not be supported on most devices. Can only - * pick from supported list - * </p> + * <p>The ratio of lens focal length to the effective + * aperture diameter.</p> + * <p>This will only be supported on the camera devices that + * have variable aperture lens. The aperture value can only be + * one of the values listed in {@link CameraCharacteristics#LENS_INFO_AVAILABLE_APERTURES android.lens.info.availableApertures}.</p> + * <p>When this is supported and {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} is OFF, + * this can be set along with {@link CaptureRequest#SENSOR_EXPOSURE_TIME android.sensor.exposureTime}, + * {@link CaptureRequest#SENSOR_SENSITIVITY android.sensor.sensitivity}, and {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration} + * to achieve manual exposure control.</p> + * <p>The requested aperture value may take several frames to reach the + * requested value; the camera device will report the current (intermediate) + * aperture size in capture result metadata while the aperture is changing. + * While the aperture is still changing, {@link CaptureResult#LENS_STATE android.lens.state} will be set to MOVING.</p> + * <p>When this is supported and {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} is one of + * the ON modes, this will be overridden by the camera device + * auto-exposure algorithm, the overridden values are then provided + * back to the user in the corresponding result.</p> + * + * @see CaptureRequest#CONTROL_AE_MODE + * @see CameraCharacteristics#LENS_INFO_AVAILABLE_APERTURES + * @see CaptureResult#LENS_STATE + * @see CaptureRequest#SENSOR_EXPOSURE_TIME + * @see CaptureRequest#SENSOR_FRAME_DURATION + * @see CaptureRequest#SENSOR_SENSITIVITY */ public static final Key<Float> LENS_APERTURE = new Key<Float>("android.lens.aperture", float.class); /** - * <p> - * State of lens neutral density - * filter(s) - * </p> - * <p> - * Will not be supported on most devices. Can only - * pick from supported list - * </p> + * <p>State of lens neutral density filter(s).</p> + * <p>This will not be supported on most camera devices. On devices + * where this is supported, this may only be set to one of the + * values included in {@link CameraCharacteristics#LENS_INFO_AVAILABLE_FILTER_DENSITIES android.lens.info.availableFilterDensities}.</p> + * <p>Lens filters are typically used to lower the amount of light the + * sensor is exposed to (measured in steps of EV). As used here, an EV + * step is the standard logarithmic representation, which are + * non-negative, and inversely proportional to the amount of light + * hitting the sensor. For example, setting this to 0 would result + * in no reduction of the incoming light, and setting this to 2 would + * mean that the filter is set to reduce incoming light by two stops + * (allowing 1/4 of the prior amount of light to the sensor).</p> + * <p>It may take several frames before the lens filter density changes + * to the requested value. While the filter density is still changing, + * {@link CaptureResult#LENS_STATE android.lens.state} will be set to MOVING.</p> + * + * @see CameraCharacteristics#LENS_INFO_AVAILABLE_FILTER_DENSITIES + * @see CaptureResult#LENS_STATE */ public static final Key<Float> LENS_FILTER_DENSITY = new Key<Float>("android.lens.filterDensity", float.class); /** - * <p> - * Lens optical zoom setting - * </p> - * <p> - * Will not be supported on most devices. - * </p> + * <p>The current lens focal length; used for optical zoom.</p> + * <p>This setting controls the physical focal length of the camera + * device's lens. Changing the focal length changes the field of + * view of the camera device, and is usually used for optical zoom.</p> + * <p>Like {@link CaptureRequest#LENS_FOCUS_DISTANCE android.lens.focusDistance} and {@link CaptureRequest#LENS_APERTURE android.lens.aperture}, this + * setting won't be applied instantaneously, and it may take several + * frames before the lens can change to the requested focal length. + * While the focal length is still changing, {@link CaptureResult#LENS_STATE android.lens.state} will + * be set to MOVING.</p> + * <p>This is expected not to be supported on most devices.</p> + * + * @see CaptureRequest#LENS_APERTURE + * @see CaptureRequest#LENS_FOCUS_DISTANCE + * @see CaptureResult#LENS_STATE */ public static final Key<Float> LENS_FOCAL_LENGTH = new Key<Float>("android.lens.focalLength", float.class); /** - * <p> - * Distance to plane of sharpest focus, - * measured from frontmost surface of the lens - * </p> - * <p> - * 0 = infinity focus. Used value should be clamped - * to (0,minimum focus distance) - * </p> + * <p>Distance to plane of sharpest focus, + * measured from frontmost surface of the lens</p> + * <p>0 means infinity focus. Used value will be clamped + * to [0, {@link CameraCharacteristics#LENS_INFO_MINIMUM_FOCUS_DISTANCE android.lens.info.minimumFocusDistance}].</p> + * <p>Like {@link CaptureRequest#LENS_FOCAL_LENGTH android.lens.focalLength}, this setting won't be applied + * instantaneously, and it may take several frames before the lens + * can move to the requested focus distance. While the lens is still moving, + * {@link CaptureResult#LENS_STATE android.lens.state} will be set to MOVING.</p> + * + * @see CaptureRequest#LENS_FOCAL_LENGTH + * @see CameraCharacteristics#LENS_INFO_MINIMUM_FOCUS_DISTANCE + * @see CaptureResult#LENS_STATE */ public static final Key<Float> LENS_FOCUS_DISTANCE = new Key<Float>("android.lens.focusDistance", float.class); /** - * <p> - * Whether optical image stabilization is - * enabled. - * </p> - * <p> - * Will not be supported on most devices. - * </p> + * <p>Sets whether the camera device uses optical image stabilization (OIS) + * when capturing images.</p> + * <p>OIS is used to compensate for motion blur due to small movements of + * the camera during capture. Unlike digital image stabilization, OIS makes + * use of mechanical elements to stabilize the camera sensor, and thus + * allows for longer exposure times before camera shake becomes + * apparent.</p> + * <p>This is not expected to be supported on most devices.</p> * @see #LENS_OPTICAL_STABILIZATION_MODE_OFF * @see #LENS_OPTICAL_STABILIZATION_MODE_ON */ @@ -865,10 +1055,19 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { new Key<Integer>("android.lens.opticalStabilizationMode", int.class); /** - * <p> - * Mode of operation for the noise reduction - * algorithm - * </p> + * <p>Mode of operation for the noise reduction + * algorithm</p> + * <p>Noise filtering control. OFF means no noise reduction + * will be applied by the camera device.</p> + * <p>This must be set to a valid mode in + * {@link CameraCharacteristics#NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES android.noiseReduction.availableNoiseReductionModes}.</p> + * <p>FAST/HIGH_QUALITY both mean camera device determined noise filtering + * will be applied. HIGH_QUALITY mode indicates that the camera device + * will use the highest-quality noise filtering algorithms, + * even if it slows down capture rate. FAST means the camera device should not + * slow down capture rate when applying noise filtering.</p> + * + * @see CameraCharacteristics#NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES * @see #NOISE_REDUCTION_MODE_OFF * @see #NOISE_REDUCTION_MODE_FAST * @see #NOISE_REDUCTION_MODE_HIGH_QUALITY @@ -877,44 +1076,33 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { new Key<Integer>("android.noiseReduction.mode", int.class); /** - * <p> - * An application-specified ID for the current + * <p>An application-specified ID for the current * request. Must be maintained unchanged in output - * frame - * </p> - * + * frame</p> * @hide */ public static final Key<Integer> REQUEST_ID = new Key<Integer>("android.request.id", int.class); /** - * <p> - * (x, y, width, height). - * </p><p> - * A rectangle with the top-level corner of (x,y) and size + * <p>(x, y, width, height).</p> + * <p>A rectangle with the top-level corner of (x,y) and size * (width, height). The region of the sensor that is used for * output. Each stream must use this rectangle to produce its * output, cropping to a smaller region if necessary to - * maintain the stream's aspect ratio. - * </p><p> - * HAL2.x uses only (x, y, width) - * </p> - * <p> - * Any additional per-stream cropping must be done to - * maximize the final pixel area of the stream. - * </p><p> - * For example, if the crop region is set to a 4:3 aspect + * maintain the stream's aspect ratio.</p> + * <p>HAL2.x uses only (x, y, width)</p> + * <p>Any additional per-stream cropping must be done to + * maximize the final pixel area of the stream.</p> + * <p>For example, if the crop region is set to a 4:3 aspect * ratio, then 4:3 streams should use the exact crop * region. 16:9 streams should further crop vertically - * (letterbox). - * </p><p> - * Conversely, if the crop region is set to a 16:9, then 4:3 + * (letterbox).</p> + * <p>Conversely, if the crop region is set to a 16:9, then 4:3 * outputs should crop horizontally (pillarbox), and 16:9 * streams should match exactly. These additional crops must - * be centered within the crop region. - * </p><p> - * The output streams must maintain square pixels at all + * be centered within the crop region.</p> + * <p>The output streams must maintain square pixels at all * times, no matter what the relative aspect ratios of the * crop region and the stream are. Negative values for * corner are allowed for raw output if full pixel array is @@ -923,69 +1111,188 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { * for raw output, where only a few fixed scales may be * possible. The width and height of the crop region cannot * be set to be smaller than floor( activeArraySize.width / - * android.scaler.maxDigitalZoom ) and floor( - * activeArraySize.height / android.scaler.maxDigitalZoom), - * respectively. - * </p> + * {@link CameraCharacteristics#SCALER_AVAILABLE_MAX_DIGITAL_ZOOM android.scaler.availableMaxDigitalZoom} ) and floor( + * activeArraySize.height / + * {@link CameraCharacteristics#SCALER_AVAILABLE_MAX_DIGITAL_ZOOM android.scaler.availableMaxDigitalZoom}), respectively.</p> + * + * @see CameraCharacteristics#SCALER_AVAILABLE_MAX_DIGITAL_ZOOM */ public static final Key<android.graphics.Rect> SCALER_CROP_REGION = new Key<android.graphics.Rect>("android.scaler.cropRegion", android.graphics.Rect.class); /** - * <p> - * Duration each pixel is exposed to - * light. - * </p><p> - * If the sensor can't expose this exact duration, it should shorten the - * duration exposed to the nearest possible value (rather than expose longer). - * </p> - * <p> - * 1/10000 - 30 sec range. No bulb mode - * </p> + * <p>Duration each pixel is exposed to + * light.</p> + * <p>If the sensor can't expose this exact duration, it should shorten the + * duration exposed to the nearest possible value (rather than expose longer).</p> */ public static final Key<Long> SENSOR_EXPOSURE_TIME = new Key<Long>("android.sensor.exposureTime", long.class); /** - * <p> - * Duration from start of frame exposure to - * start of next frame exposure - * </p> - * <p> - * Exposure time has priority, so duration is set to - * max(duration, exposure time + overhead) - * </p> + * <p>Duration from start of frame exposure to + * start of next frame exposure.</p> + * <p>The maximum frame rate that can be supported by a camera subsystem is + * a function of many factors:</p> + * <ul> + * <li>Requested resolutions of output image streams</li> + * <li>Availability of binning / skipping modes on the imager</li> + * <li>The bandwidth of the imager interface</li> + * <li>The bandwidth of the various ISP processing blocks</li> + * </ul> + * <p>Since these factors can vary greatly between different ISPs and + * sensors, the camera abstraction tries to represent the bandwidth + * restrictions with as simple a model as possible.</p> + * <p>The model presented has the following characteristics:</p> + * <ul> + * <li>The image sensor is always configured to output the smallest + * resolution possible given the application's requested output stream + * sizes. The smallest resolution is defined as being at least as large + * as the largest requested output stream size; the camera pipeline must + * never digitally upsample sensor data when the crop region covers the + * whole sensor. In general, this means that if only small output stream + * resolutions are configured, the sensor can provide a higher frame + * rate.</li> + * <li>Since any request may use any or all the currently configured + * output streams, the sensor and ISP must be configured to support + * scaling a single capture to all the streams at the same time. This + * means the camera pipeline must be ready to produce the largest + * requested output size without any delay. Therefore, the overall + * frame rate of a given configured stream set is governed only by the + * largest requested stream resolution.</li> + * <li>Using more than one output stream in a request does not affect the + * frame duration.</li> + * <li>Certain format-streams may need to do additional background processing + * before data is consumed/produced by that stream. These processors + * can run concurrently to the rest of the camera pipeline, but + * cannot process more than 1 capture at a time.</li> + * </ul> + * <p>The necessary information for the application, given the model above, + * is provided via the {@link CameraCharacteristics#SCALER_AVAILABLE_MIN_FRAME_DURATIONS android.scaler.availableMinFrameDurations} field. + * These are used to determine the maximum frame rate / minimum frame + * duration that is possible for a given stream configuration.</p> + * <p>Specifically, the application can use the following rules to + * determine the minimum frame duration it can request from the camera + * device:</p> + * <ol> + * <li>Let the set of currently configured input/output streams + * be called <code>S</code>.</li> + * <li>Find the minimum frame durations for each stream in <code>S</code>, by + * looking it up in {@link CameraCharacteristics#SCALER_AVAILABLE_MIN_FRAME_DURATIONS android.scaler.availableMinFrameDurations} (with + * its respective size/format). Let this set of frame durations be called + * <code>F</code>.</li> + * <li>For any given request <code>R</code>, the minimum frame duration allowed + * for <code>R</code> is the maximum out of all values in <code>F</code>. Let the streams + * used in <code>R</code> be called <code>S_r</code>.</li> + * </ol> + * <p>If none of the streams in <code>S_r</code> have a stall time (listed in + * {@link CameraCharacteristics#SCALER_AVAILABLE_STALL_DURATIONS android.scaler.availableStallDurations}), then the frame duration in + * <code>F</code> determines the steady state frame rate that the application will + * get if it uses <code>R</code> as a repeating request. Let this special kind + * of request be called <code>Rsimple</code>.</p> + * <p>A repeating request <code>Rsimple</code> can be <em>occasionally</em> interleaved + * by a single capture of a new request <code>Rstall</code> (which has at least + * one in-use stream with a non-0 stall time) and if <code>Rstall</code> has the + * same minimum frame duration this will not cause a frame rate loss + * if all buffers from the previous <code>Rstall</code> have already been + * delivered.</p> + * <p>For more details about stalling, see + * {@link CameraCharacteristics#SCALER_AVAILABLE_STALL_DURATIONS android.scaler.availableStallDurations}.</p> + * + * @see CameraCharacteristics#SCALER_AVAILABLE_MIN_FRAME_DURATIONS + * @see CameraCharacteristics#SCALER_AVAILABLE_STALL_DURATIONS */ public static final Key<Long> SENSOR_FRAME_DURATION = new Key<Long>("android.sensor.frameDuration", long.class); /** - * <p> - * Gain applied to image data. Must be + * <p>Gain applied to image data. Must be * implemented through analog gain only if set to values - * below 'maximum analog sensitivity'. - * </p><p> - * If the sensor can't apply this exact gain, it should lessen the - * gain to the nearest possible value (rather than gain more). - * </p> - * <p> - * ISO 12232:2006 REI method - * </p> + * below 'maximum analog sensitivity'.</p> + * <p>If the sensor can't apply this exact gain, it should lessen the + * gain to the nearest possible value (rather than gain more).</p> + * <p>ISO 12232:2006 REI method</p> */ public static final Key<Integer> SENSOR_SENSITIVITY = new Key<Integer>("android.sensor.sensitivity", int.class); /** - * <p> - * State of the face detector - * unit - * </p> - * <p> - * Whether face detection is enabled, and whether it + * <p>A pixel <code>[R, G_even, G_odd, B]</code> that supplies the test pattern + * when {@link CaptureRequest#SENSOR_TEST_PATTERN_MODE android.sensor.testPatternMode} is SOLID_COLOR.</p> + * <p>Each color channel is treated as an unsigned 32-bit integer. + * The camera device then uses the most significant X bits + * that correspond to how many bits are in its Bayer raw sensor + * output.</p> + * <p>For example, a sensor with RAW10 Bayer output would use the + * 10 most significant bits from each color channel.</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + * + * @see CaptureRequest#SENSOR_TEST_PATTERN_MODE + */ + public static final Key<int[]> SENSOR_TEST_PATTERN_DATA = + new Key<int[]>("android.sensor.testPatternData", int[].class); + + /** + * <p>When enabled, the sensor sends a test pattern instead of + * doing a real exposure from the camera.</p> + * <p>When a test pattern is enabled, all manual sensor controls specified + * by android.sensor.* should be ignored. All other controls should + * work as normal.</p> + * <p>For example, if manual flash is enabled, flash firing should still + * occur (and that the test pattern remain unmodified, since the flash + * would not actually affect it).</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + * @see #SENSOR_TEST_PATTERN_MODE_OFF + * @see #SENSOR_TEST_PATTERN_MODE_SOLID_COLOR + * @see #SENSOR_TEST_PATTERN_MODE_COLOR_BARS + * @see #SENSOR_TEST_PATTERN_MODE_COLOR_BARS_FADE_TO_GRAY + * @see #SENSOR_TEST_PATTERN_MODE_PN9 + * @see #SENSOR_TEST_PATTERN_MODE_CUSTOM1 + */ + public static final Key<Integer> SENSOR_TEST_PATTERN_MODE = + new Key<Integer>("android.sensor.testPatternMode", int.class); + + /** + * <p>Quality of lens shading correction applied + * to the image data.</p> + * <p>When set to OFF mode, no lens shading correction will be applied by the + * camera device, and an identity lens shading map data will be provided + * if <code>{@link CaptureRequest#STATISTICS_LENS_SHADING_MAP_MODE android.statistics.lensShadingMapMode} == ON</code>. For example, for lens + * shading map with size specified as <code>{@link CameraCharacteristics#LENS_INFO_SHADING_MAP_SIZE android.lens.info.shadingMapSize} = [ 4, 3 ]</code>, + * the output {@link CaptureResult#STATISTICS_LENS_SHADING_MAP android.statistics.lensShadingMap} for this case will be an identity map + * shown below:</p> + * <pre><code>[ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + * 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + * 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + * 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + * 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + * 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 ] + * </code></pre> + * <p>When set to other modes, lens shading correction will be applied by the + * camera device. Applications can request lens shading map data by setting + * {@link CaptureRequest#STATISTICS_LENS_SHADING_MAP_MODE android.statistics.lensShadingMapMode} to ON, and then the camera device will provide + * lens shading map data in {@link CaptureResult#STATISTICS_LENS_SHADING_MAP android.statistics.lensShadingMap}, with size specified + * by {@link CameraCharacteristics#LENS_INFO_SHADING_MAP_SIZE android.lens.info.shadingMapSize}.</p> + * + * @see CameraCharacteristics#LENS_INFO_SHADING_MAP_SIZE + * @see CaptureResult#STATISTICS_LENS_SHADING_MAP + * @see CaptureRequest#STATISTICS_LENS_SHADING_MAP_MODE + * @see #SHADING_MODE_OFF + * @see #SHADING_MODE_FAST + * @see #SHADING_MODE_HIGH_QUALITY + */ + public static final Key<Integer> SHADING_MODE = + new Key<Integer>("android.shading.mode", int.class); + + /** + * <p>State of the face detector + * unit</p> + * <p>Whether face detection is enabled, and whether it * should output just the basic fields or the full set of * fields. Value must be one of the - * android.statistics.info.availableFaceDetectModes. - * </p> + * {@link CameraCharacteristics#STATISTICS_INFO_AVAILABLE_FACE_DETECT_MODES android.statistics.info.availableFaceDetectModes}.</p> + * + * @see CameraCharacteristics#STATISTICS_INFO_AVAILABLE_FACE_DETECT_MODES * @see #STATISTICS_FACE_DETECT_MODE_OFF * @see #STATISTICS_FACE_DETECT_MODE_SIMPLE * @see #STATISTICS_FACE_DETECT_MODE_FULL @@ -994,15 +1301,25 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { new Key<Integer>("android.statistics.faceDetectMode", int.class); /** - * <p> - * Whether the HAL needs to output the lens - * shading map in output result metadata - * </p> - * <p> - * When set to ON, - * android.statistics.lensShadingMap must be provided in - * the output result metdata. - * </p> + * <p>Operating mode for hotpixel map generation.</p> + * <p>If set to ON, a hotpixel map is returned in {@link CaptureResult#STATISTICS_HOT_PIXEL_MAP android.statistics.hotPixelMap}. + * If set to OFF, no hotpixel map should be returned.</p> + * <p>This must be set to a valid mode from {@link CameraCharacteristics#STATISTICS_INFO_AVAILABLE_HOT_PIXEL_MAP_MODES android.statistics.info.availableHotPixelMapModes}.</p> + * + * @see CaptureResult#STATISTICS_HOT_PIXEL_MAP + * @see CameraCharacteristics#STATISTICS_INFO_AVAILABLE_HOT_PIXEL_MAP_MODES + */ + public static final Key<Boolean> STATISTICS_HOT_PIXEL_MAP_MODE = + new Key<Boolean>("android.statistics.hotPixelMapMode", boolean.class); + + /** + * <p>Whether the camera device will output the lens + * shading map in output result metadata.</p> + * <p>When set to ON, + * {@link CaptureResult#STATISTICS_LENS_SHADING_MAP android.statistics.lensShadingMap} must be provided in + * the output result metadata.</p> + * + * @see CaptureResult#STATISTICS_LENS_SHADING_MAP * @see #STATISTICS_LENS_SHADING_MAP_MODE_OFF * @see #STATISTICS_LENS_SHADING_MAP_MODE_ON */ @@ -1010,61 +1327,110 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { new Key<Integer>("android.statistics.lensShadingMapMode", int.class); /** - * <p> - * Table mapping blue input values to output - * values - * </p> - * <p> - * Tonemapping / contrast / gamma curve for the blue - * channel, to use when android.tonemap.mode is CONTRAST_CURVE. - * </p><p> - * See android.tonemap.curveRed for more details. - * </p> + * <p>Tonemapping / contrast / gamma curve for the blue + * channel, to use when {@link CaptureRequest#TONEMAP_MODE android.tonemap.mode} is + * CONTRAST_CURVE.</p> + * <p>See {@link CaptureRequest#TONEMAP_CURVE_RED android.tonemap.curveRed} for more details.</p> + * + * @see CaptureRequest#TONEMAP_CURVE_RED + * @see CaptureRequest#TONEMAP_MODE */ public static final Key<float[]> TONEMAP_CURVE_BLUE = new Key<float[]>("android.tonemap.curveBlue", float[].class); /** - * <p> - * Table mapping green input values to output - * values - * </p> - * <p> - * Tonemapping / contrast / gamma curve for the green - * channel, to use when android.tonemap.mode is CONTRAST_CURVE. - * </p><p> - * See android.tonemap.curveRed for more details. - * </p> + * <p>Tonemapping / contrast / gamma curve for the green + * channel, to use when {@link CaptureRequest#TONEMAP_MODE android.tonemap.mode} is + * CONTRAST_CURVE.</p> + * <p>See {@link CaptureRequest#TONEMAP_CURVE_RED android.tonemap.curveRed} for more details.</p> + * + * @see CaptureRequest#TONEMAP_CURVE_RED + * @see CaptureRequest#TONEMAP_MODE */ public static final Key<float[]> TONEMAP_CURVE_GREEN = new Key<float[]>("android.tonemap.curveGreen", float[].class); /** - * <p> - * Table mapping red input values to output - * values - * </p> - * <p> - * Tonemapping / contrast / gamma curve for the red - * channel, to use when android.tonemap.mode is CONTRAST_CURVE. - * </p><p> - * Since the input and output ranges may vary depending on - * the camera pipeline, the input and output pixel values - * are represented by normalized floating-point values - * between 0 and 1, with 0 == black and 1 == white. - * </p><p> - * The curve should be linearly interpolated between the - * defined points. The points will be listed in increasing - * order of P_IN. For example, if the array is: [0.0, 0.0, - * 0.3, 0.5, 1.0, 1.0], then the input->output mapping - * for a few sample points would be: 0 -> 0, 0.15 -> - * 0.25, 0.3 -> 0.5, 0.5 -> 0.64 - * </p> + * <p>Tonemapping / contrast / gamma curve for the red + * channel, to use when {@link CaptureRequest#TONEMAP_MODE android.tonemap.mode} is + * CONTRAST_CURVE.</p> + * <p>Each channel's curve is defined by an array of control points:</p> + * <pre><code>{@link CaptureRequest#TONEMAP_CURVE_RED android.tonemap.curveRed} = + * [ P0in, P0out, P1in, P1out, P2in, P2out, P3in, P3out, ..., PNin, PNout ] + * 2 <= N <= {@link CameraCharacteristics#TONEMAP_MAX_CURVE_POINTS android.tonemap.maxCurvePoints}</code></pre> + * <p>These are sorted in order of increasing <code>Pin</code>; it is always + * guaranteed that input values 0.0 and 1.0 are included in the list to + * define a complete mapping. For input values between control points, + * the camera device must linearly interpolate between the control + * points.</p> + * <p>Each curve can have an independent number of points, and the number + * of points can be less than max (that is, the request doesn't have to + * always provide a curve with number of points equivalent to + * {@link CameraCharacteristics#TONEMAP_MAX_CURVE_POINTS android.tonemap.maxCurvePoints}).</p> + * <p>A few examples, and their corresponding graphical mappings; these + * only specify the red channel and the precision is limited to 4 + * digits, for conciseness.</p> + * <p>Linear mapping:</p> + * <pre><code>{@link CaptureRequest#TONEMAP_CURVE_RED android.tonemap.curveRed} = [ 0, 0, 1.0, 1.0 ] + * </code></pre> + * <p><img alt="Linear mapping curve" src="../../../../images/camera2/metadata/android.tonemap.curveRed/linear_tonemap.png" /></p> + * <p>Invert mapping:</p> + * <pre><code>{@link CaptureRequest#TONEMAP_CURVE_RED android.tonemap.curveRed} = [ 0, 1.0, 1.0, 0 ] + * </code></pre> + * <p><img alt="Inverting mapping curve" src="../../../../images/camera2/metadata/android.tonemap.curveRed/inverse_tonemap.png" /></p> + * <p>Gamma 1/2.2 mapping, with 16 control points:</p> + * <pre><code>{@link CaptureRequest#TONEMAP_CURVE_RED android.tonemap.curveRed} = [ + * 0.0000, 0.0000, 0.0667, 0.2920, 0.1333, 0.4002, 0.2000, 0.4812, + * 0.2667, 0.5484, 0.3333, 0.6069, 0.4000, 0.6594, 0.4667, 0.7072, + * 0.5333, 0.7515, 0.6000, 0.7928, 0.6667, 0.8317, 0.7333, 0.8685, + * 0.8000, 0.9035, 0.8667, 0.9370, 0.9333, 0.9691, 1.0000, 1.0000 ] + * </code></pre> + * <p><img alt="Gamma = 1/2.2 tonemapping curve" src="../../../../images/camera2/metadata/android.tonemap.curveRed/gamma_tonemap.png" /></p> + * <p>Standard sRGB gamma mapping, per IEC 61966-2-1:1999, with 16 control points:</p> + * <pre><code>{@link CaptureRequest#TONEMAP_CURVE_RED android.tonemap.curveRed} = [ + * 0.0000, 0.0000, 0.0667, 0.2864, 0.1333, 0.4007, 0.2000, 0.4845, + * 0.2667, 0.5532, 0.3333, 0.6125, 0.4000, 0.6652, 0.4667, 0.7130, + * 0.5333, 0.7569, 0.6000, 0.7977, 0.6667, 0.8360, 0.7333, 0.8721, + * 0.8000, 0.9063, 0.8667, 0.9389, 0.9333, 0.9701, 1.0000, 1.0000 ] + * </code></pre> + * <p><img alt="sRGB tonemapping curve" src="../../../../images/camera2/metadata/android.tonemap.curveRed/srgb_tonemap.png" /></p> + * + * @see CaptureRequest#TONEMAP_CURVE_RED + * @see CameraCharacteristics#TONEMAP_MAX_CURVE_POINTS + * @see CaptureRequest#TONEMAP_MODE */ public static final Key<float[]> TONEMAP_CURVE_RED = new Key<float[]>("android.tonemap.curveRed", float[].class); /** + * <p>High-level global contrast/gamma/tonemapping control.</p> + * <p>When switching to an application-defined contrast curve by setting + * {@link CaptureRequest#TONEMAP_MODE android.tonemap.mode} to CONTRAST_CURVE, the curve is defined + * per-channel with a set of <code>(in, out)</code> points that specify the + * mapping from input high-bit-depth pixel value to the output + * low-bit-depth value. Since the actual pixel ranges of both input + * and output may change depending on the camera pipeline, the values + * are specified by normalized floating-point numbers.</p> + * <p>More-complex color mapping operations such as 3D color look-up + * tables, selective chroma enhancement, or other non-linear color + * transforms will be disabled when {@link CaptureRequest#TONEMAP_MODE android.tonemap.mode} is + * CONTRAST_CURVE.</p> + * <p>This must be set to a valid mode in + * {@link CameraCharacteristics#TONEMAP_AVAILABLE_TONE_MAP_MODES android.tonemap.availableToneMapModes}.</p> + * <p>When using either FAST or HIGH_QUALITY, the camera device will + * emit its own tonemap curve in {@link CaptureRequest#TONEMAP_CURVE_RED android.tonemap.curveRed}, + * {@link CaptureRequest#TONEMAP_CURVE_GREEN android.tonemap.curveGreen}, and {@link CaptureRequest#TONEMAP_CURVE_BLUE android.tonemap.curveBlue}. + * These values are always available, and as close as possible to the + * actually used nonlinear/nonglobal transforms.</p> + * <p>If a request is sent with TRANSFORM_MATRIX with the camera device's + * provided curve in FAST or HIGH_QUALITY, the image's tonemap will be + * roughly the same.</p> + * + * @see CameraCharacteristics#TONEMAP_AVAILABLE_TONE_MAP_MODES + * @see CaptureRequest#TONEMAP_CURVE_BLUE + * @see CaptureRequest#TONEMAP_CURVE_GREEN + * @see CaptureRequest#TONEMAP_CURVE_RED + * @see CaptureRequest#TONEMAP_MODE * @see #TONEMAP_MODE_CONTRAST_CURVE * @see #TONEMAP_MODE_FAST * @see #TONEMAP_MODE_HIGH_QUALITY @@ -1073,49 +1439,59 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { new Key<Integer>("android.tonemap.mode", int.class); /** - * <p> - * This LED is nominally used to indicate to the user + * <p>This LED is nominally used to indicate to the user * that the camera is powered on and may be streaming images back to the * Application Processor. In certain rare circumstances, the OS may * disable this when video is processed locally and not transmitted to - * any untrusted applications. - * </p><p> - * In particular, the LED *must* always be on when the data could be - * transmitted off the device. The LED *should* always be on whenever - * data is stored locally on the device. - * </p><p> - * The LED *may* be off if a trusted application is using the data that - * doesn't violate the above rules. - * </p> - * + * any untrusted applications.</p> + * <p>In particular, the LED <em>must</em> always be on when the data could be + * transmitted off the device. The LED <em>should</em> always be on whenever + * data is stored locally on the device.</p> + * <p>The LED <em>may</em> be off if a trusted application is using the data that + * doesn't violate the above rules.</p> * @hide */ public static final Key<Boolean> LED_TRANSMIT = new Key<Boolean>("android.led.transmit", boolean.class); /** - * <p> - * Whether black-level compensation is locked - * to its current values, or is free to vary - * </p> - * <p> - * When set to ON, the values used for black-level - * compensation must not change until the lock is set to - * OFF - * </p><p> - * Since changes to certain capture parameters (such as + * <p>Whether black-level compensation is locked + * to its current values, or is free to vary.</p> + * <p>When set to ON, the values used for black-level + * compensation will not change until the lock is set to + * OFF.</p> + * <p>Since changes to certain capture parameters (such as * exposure time) may require resetting of black level - * compensation, the HAL must report whether setting the - * black level lock was successful in the output result - * metadata. - * </p><p> - * The black level locking must happen at the sensor, and not at the ISP. - * If for some reason black level locking is no longer legal (for example, - * the analog gain has changed, which forces black levels to be - * recalculated), then the HAL is free to override this request (and it - * must report 'OFF' when this does happen) until the next time locking - * is legal again. - * </p> + * compensation, the camera device must report whether setting + * the black level lock was successful in the output result + * metadata.</p> + * <p>For example, if a sequence of requests is as follows:</p> + * <ul> + * <li>Request 1: Exposure = 10ms, Black level lock = OFF</li> + * <li>Request 2: Exposure = 10ms, Black level lock = ON</li> + * <li>Request 3: Exposure = 10ms, Black level lock = ON</li> + * <li>Request 4: Exposure = 20ms, Black level lock = ON</li> + * <li>Request 5: Exposure = 20ms, Black level lock = ON</li> + * <li>Request 6: Exposure = 20ms, Black level lock = ON</li> + * </ul> + * <p>And the exposure change in Request 4 requires the camera + * device to reset the black level offsets, then the output + * result metadata is expected to be:</p> + * <ul> + * <li>Result 1: Exposure = 10ms, Black level lock = OFF</li> + * <li>Result 2: Exposure = 10ms, Black level lock = ON</li> + * <li>Result 3: Exposure = 10ms, Black level lock = ON</li> + * <li>Result 4: Exposure = 20ms, Black level lock = OFF</li> + * <li>Result 5: Exposure = 20ms, Black level lock = ON</li> + * <li>Result 6: Exposure = 20ms, Black level lock = ON</li> + * </ul> + * <p>This indicates to the application that on frame 4, black + * levels were reset due to exposure value changes, and pixel + * values may not be consistent across captures.</p> + * <p>The camera device will maintain the lock to the extent + * possible, only overriding the lock to OFF when changes to + * other request parameters require a black level recalculation + * or reset.</p> */ public static final Key<Boolean> BLACK_LEVEL_LOCK = new Key<Boolean>("android.blackLevel.lock", boolean.class); diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java index 535b963..b3bce3b 100644 --- a/core/java/android/hardware/camera2/CaptureResult.java +++ b/core/java/android/hardware/camera2/CaptureResult.java @@ -16,8 +16,6 @@ package android.hardware.camera2; -import android.graphics.Point; -import android.graphics.Rect; import android.hardware.camera2.impl.CameraMetadataNative; /** @@ -124,104 +122,308 @@ public final class CaptureResult extends CameraMetadata { * modify the comment blocks at the start or end. *~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~*/ + /** - * <p> - * A color transform matrix to use to transform - * from sensor RGB color space to output linear sRGB color space - * </p> - * <p> - * This matrix is either set by HAL when the request - * android.colorCorrection.mode is not TRANSFORM_MATRIX, or + * <p>A color transform matrix to use to transform + * from sensor RGB color space to output linear sRGB color space</p> + * <p>This matrix is either set by the camera device when the request + * {@link CaptureRequest#COLOR_CORRECTION_MODE android.colorCorrection.mode} is not TRANSFORM_MATRIX, or * directly by the application in the request when the - * android.colorCorrection.mode is TRANSFORM_MATRIX. - * </p><p> - * In the latter case, the HAL may round the matrix to account - * for precision issues; the final rounded matrix should be - * reported back in this matrix result metadata. - * </p> + * {@link CaptureRequest#COLOR_CORRECTION_MODE android.colorCorrection.mode} is TRANSFORM_MATRIX.</p> + * <p>In the latter case, the camera device may round the matrix to account + * for precision issues; the final rounded matrix should be reported back + * in this matrix result metadata. The transform should keep the magnitude + * of the output color values within <code>[0, 1.0]</code> (assuming input color + * values is within the normalized range <code>[0, 1.0]</code>), or clipping may occur.</p> + * + * @see CaptureRequest#COLOR_CORRECTION_MODE */ public static final Key<Rational[]> COLOR_CORRECTION_TRANSFORM = new Key<Rational[]>("android.colorCorrection.transform", Rational[].class); /** - * <p> - * Gains applying to Bayer color channels for - * white-balance - * </p> - * <p> - * The 4-channel white-balance gains are defined in - * the order of [R G_even G_odd B], where G_even is the gain - * for green pixels on even rows of the output, and G_odd - * is the gain for greenpixels on the odd rows. if a HAL + * <p>Gains applying to Bayer raw color channels for + * white-balance.</p> + * <p>The 4-channel white-balance gains are defined in + * the order of <code>[R G_even G_odd B]</code>, where <code>G_even</code> is the gain + * for green pixels on even rows of the output, and <code>G_odd</code> + * is the gain for green pixels on the odd rows. if a HAL * does not support a separate gain for even/odd green channels, - * it should use the G_even value,and write G_odd equal to - * G_even in the output result metadata. - * </p><p> - * This array is either set by HAL when the request - * android.colorCorrection.mode is not TRANSFORM_MATRIX, or + * it should use the <code>G_even</code> value, and write <code>G_odd</code> equal to + * <code>G_even</code> in the output result metadata.</p> + * <p>This array is either set by the camera device when the request + * {@link CaptureRequest#COLOR_CORRECTION_MODE android.colorCorrection.mode} is not TRANSFORM_MATRIX, or * directly by the application in the request when the - * android.colorCorrection.mode is TRANSFORM_MATRIX. - * </p><p> - * The ouput should be the gains actually applied by the HAL to - * the current frame. - * </p> + * {@link CaptureRequest#COLOR_CORRECTION_MODE android.colorCorrection.mode} is TRANSFORM_MATRIX.</p> + * <p>The output should be the gains actually applied by the camera device to + * the current frame.</p> + * + * @see CaptureRequest#COLOR_CORRECTION_MODE */ public static final Key<float[]> COLOR_CORRECTION_GAINS = new Key<float[]>("android.colorCorrection.gains", float[].class); /** - * <p> - * The ID sent with the latest - * CAMERA2_TRIGGER_PRECAPTURE_METERING call - * </p> - * <p> - * Must be 0 if no + * <p>The ID sent with the latest + * CAMERA2_TRIGGER_PRECAPTURE_METERING call</p> + * <p>Must be 0 if no * CAMERA2_TRIGGER_PRECAPTURE_METERING trigger received yet * by HAL. Always updated even if AE algorithm ignores the - * trigger - * </p> - * + * trigger</p> * @hide */ public static final Key<Integer> CONTROL_AE_PRECAPTURE_ID = new Key<Integer>("android.control.aePrecaptureId", int.class); /** - * <p> - * List of areas to use for - * metering - * </p> - * <p> - * Each area is a rectangle plus weight: xmin, ymin, - * xmax, ymax, weight. The rectangle is defined inclusive of the - * specified coordinates. - * </p><p> - * The coordinate system is based on the active pixel array, + * <p>The desired mode for the camera device's + * auto-exposure routine.</p> + * <p>This control is only effective if {@link CaptureRequest#CONTROL_MODE android.control.mode} is + * AUTO.</p> + * <p>When set to any of the ON modes, the camera device's + * auto-exposure routine is enabled, overriding the + * application's selected exposure time, sensor sensitivity, + * and frame duration ({@link CaptureRequest#SENSOR_EXPOSURE_TIME android.sensor.exposureTime}, + * {@link CaptureRequest#SENSOR_SENSITIVITY android.sensor.sensitivity}, and + * {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration}). If one of the FLASH modes + * is selected, the camera device's flash unit controls are + * also overridden.</p> + * <p>The FLASH modes are only available if the camera device + * has a flash unit ({@link CameraCharacteristics#FLASH_INFO_AVAILABLE android.flash.info.available} is <code>true</code>).</p> + * <p>If flash TORCH mode is desired, this field must be set to + * ON or OFF, and {@link CaptureRequest#FLASH_MODE android.flash.mode} set to TORCH.</p> + * <p>When set to any of the ON modes, the values chosen by the + * camera device auto-exposure routine for the overridden + * fields for a given capture will be available in its + * CaptureResult.</p> + * + * @see CaptureRequest#CONTROL_MODE + * @see CameraCharacteristics#FLASH_INFO_AVAILABLE + * @see CaptureRequest#FLASH_MODE + * @see CaptureRequest#SENSOR_EXPOSURE_TIME + * @see CaptureRequest#SENSOR_FRAME_DURATION + * @see CaptureRequest#SENSOR_SENSITIVITY + * @see #CONTROL_AE_MODE_OFF + * @see #CONTROL_AE_MODE_ON + * @see #CONTROL_AE_MODE_ON_AUTO_FLASH + * @see #CONTROL_AE_MODE_ON_ALWAYS_FLASH + * @see #CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE + */ + public static final Key<Integer> CONTROL_AE_MODE = + new Key<Integer>("android.control.aeMode", int.class); + + /** + * <p>List of areas to use for + * metering.</p> + * <p>Each area is a rectangle plus weight: xmin, ymin, + * xmax, ymax, weight. The rectangle is defined to be inclusive of the + * specified coordinates.</p> + * <p>The coordinate system is based on the active pixel array, * with (0,0) being the top-left pixel in the active pixel array, and - * (android.sensor.info.activeArraySize.width - 1, - * android.sensor.info.activeArraySize.height - 1) being the + * ({@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE android.sensor.info.activeArraySize}.width - 1, + * {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE android.sensor.info.activeArraySize}.height - 1) being the * bottom-right pixel in the active pixel array. The weight - * should be nonnegative. - * </p><p> - * If all regions have 0 weight, then no specific metering area - * needs to be used by the HAL. If the metering region is - * outside the current android.scaler.cropRegion, the HAL - * should ignore the sections outside the region and output the - * used sections in the frame metadata - * </p> + * should be nonnegative.</p> + * <p>If all regions have 0 weight, then no specific metering area + * needs to be used by the camera device. If the metering region is + * outside the current {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion}, the camera device + * will ignore the sections outside the region and output the + * used sections in the frame metadata.</p> + * + * @see CaptureRequest#SCALER_CROP_REGION + * @see CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE */ public static final Key<int[]> CONTROL_AE_REGIONS = new Key<int[]>("android.control.aeRegions", int[].class); /** - * <p> - * Current state of AE algorithm - * </p> - * <p> - * Whenever the AE algorithm state changes, a - * MSG_AUTOEXPOSURE notification must be send if a - * notification callback is registered. - * </p> + * <p>Current state of AE algorithm</p> + * <p>Switching between or enabling AE modes ({@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode}) always + * resets the AE state to INACTIVE. Similarly, switching between {@link CaptureRequest#CONTROL_MODE android.control.mode}, + * or {@link CaptureRequest#CONTROL_SCENE_MODE android.control.sceneMode} if <code>{@link CaptureRequest#CONTROL_MODE android.control.mode} == USE_SCENE_MODE</code> resets all + * the algorithm states to INACTIVE.</p> + * <p>The camera device can do several state transitions between two results, if it is + * allowed by the state transition table. For example: INACTIVE may never actually be + * seen in a result.</p> + * <p>The state in the result is the state for this image (in sync with this image): if + * AE state becomes CONVERGED, then the image data associated with this result should + * be good to use.</p> + * <p>Below are state transition tables for different AE modes.</p> + * <table> + * <thead> + * <tr> + * <th align="center">State</th> + * <th align="center">Transition Cause</th> + * <th align="center">New State</th> + * <th align="center">Notes</th> + * </tr> + * </thead> + * <tbody> + * <tr> + * <td align="center">INACTIVE</td> + * <td align="center"></td> + * <td align="center">INACTIVE</td> + * <td align="center">Camera device auto exposure algorithm is disabled</td> + * </tr> + * </tbody> + * </table> + * <p>When {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} is AE_MODE_ON_*:</p> + * <table> + * <thead> + * <tr> + * <th align="center">State</th> + * <th align="center">Transition Cause</th> + * <th align="center">New State</th> + * <th align="center">Notes</th> + * </tr> + * </thead> + * <tbody> + * <tr> + * <td align="center">INACTIVE</td> + * <td align="center">Camera device initiates AE scan</td> + * <td align="center">SEARCHING</td> + * <td align="center">Values changing</td> + * </tr> + * <tr> + * <td align="center">INACTIVE</td> + * <td align="center">{@link CaptureRequest#CONTROL_AE_LOCK android.control.aeLock} is ON</td> + * <td align="center">LOCKED</td> + * <td align="center">Values locked</td> + * </tr> + * <tr> + * <td align="center">SEARCHING</td> + * <td align="center">Camera device finishes AE scan</td> + * <td align="center">CONVERGED</td> + * <td align="center">Good values, not changing</td> + * </tr> + * <tr> + * <td align="center">SEARCHING</td> + * <td align="center">Camera device finishes AE scan</td> + * <td align="center">FLASH_REQUIRED</td> + * <td align="center">Converged but too dark w/o flash</td> + * </tr> + * <tr> + * <td align="center">SEARCHING</td> + * <td align="center">{@link CaptureRequest#CONTROL_AE_LOCK android.control.aeLock} is ON</td> + * <td align="center">LOCKED</td> + * <td align="center">Values locked</td> + * </tr> + * <tr> + * <td align="center">CONVERGED</td> + * <td align="center">Camera device initiates AE scan</td> + * <td align="center">SEARCHING</td> + * <td align="center">Values changing</td> + * </tr> + * <tr> + * <td align="center">CONVERGED</td> + * <td align="center">{@link CaptureRequest#CONTROL_AE_LOCK android.control.aeLock} is ON</td> + * <td align="center">LOCKED</td> + * <td align="center">Values locked</td> + * </tr> + * <tr> + * <td align="center">FLASH_REQUIRED</td> + * <td align="center">Camera device initiates AE scan</td> + * <td align="center">SEARCHING</td> + * <td align="center">Values changing</td> + * </tr> + * <tr> + * <td align="center">FLASH_REQUIRED</td> + * <td align="center">{@link CaptureRequest#CONTROL_AE_LOCK android.control.aeLock} is ON</td> + * <td align="center">LOCKED</td> + * <td align="center">Values locked</td> + * </tr> + * <tr> + * <td align="center">LOCKED</td> + * <td align="center">{@link CaptureRequest#CONTROL_AE_LOCK android.control.aeLock} is OFF</td> + * <td align="center">SEARCHING</td> + * <td align="center">Values not good after unlock</td> + * </tr> + * <tr> + * <td align="center">LOCKED</td> + * <td align="center">{@link CaptureRequest#CONTROL_AE_LOCK android.control.aeLock} is OFF</td> + * <td align="center">CONVERGED</td> + * <td align="center">Values good after unlock</td> + * </tr> + * <tr> + * <td align="center">LOCKED</td> + * <td align="center">{@link CaptureRequest#CONTROL_AE_LOCK android.control.aeLock} is OFF</td> + * <td align="center">FLASH_REQUIRED</td> + * <td align="center">Exposure good, but too dark</td> + * </tr> + * <tr> + * <td align="center">PRECAPTURE</td> + * <td align="center">Sequence done. {@link CaptureRequest#CONTROL_AE_LOCK android.control.aeLock} is OFF</td> + * <td align="center">CONVERGED</td> + * <td align="center">Ready for high-quality capture</td> + * </tr> + * <tr> + * <td align="center">PRECAPTURE</td> + * <td align="center">Sequence done. {@link CaptureRequest#CONTROL_AE_LOCK android.control.aeLock} is ON</td> + * <td align="center">LOCKED</td> + * <td align="center">Ready for high-quality capture</td> + * </tr> + * <tr> + * <td align="center">Any state</td> + * <td align="center">{@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER android.control.aePrecaptureTrigger} is START</td> + * <td align="center">PRECAPTURE</td> + * <td align="center">Start AE precapture metering sequence</td> + * </tr> + * </tbody> + * </table> + * <p>For the above table, the camera device may skip reporting any state changes that happen + * without application intervention (i.e. mode switch, trigger, locking). Any state that + * can be skipped in that manner is called a transient state.</p> + * <p>For example, for above AE modes (AE_MODE_ON_*), in addition to the state transitions + * listed in above table, it is also legal for the camera device to skip one or more + * transient states between two results. See below table for examples:</p> + * <table> + * <thead> + * <tr> + * <th align="center">State</th> + * <th align="center">Transition Cause</th> + * <th align="center">New State</th> + * <th align="center">Notes</th> + * </tr> + * </thead> + * <tbody> + * <tr> + * <td align="center">INACTIVE</td> + * <td align="center">Camera device finished AE scan</td> + * <td align="center">CONVERGED</td> + * <td align="center">Values are already good, transient states are skipped by camera device.</td> + * </tr> + * <tr> + * <td align="center">Any state</td> + * <td align="center">{@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER android.control.aePrecaptureTrigger} is START, sequence done</td> + * <td align="center">FLASH_REQUIRED</td> + * <td align="center">Converged but too dark w/o flash after a precapture sequence, transient states are skipped by camera device.</td> + * </tr> + * <tr> + * <td align="center">Any state</td> + * <td align="center">{@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER android.control.aePrecaptureTrigger} is START, sequence done</td> + * <td align="center">CONVERGED</td> + * <td align="center">Converged after a precapture sequence, transient states are skipped by camera device.</td> + * </tr> + * <tr> + * <td align="center">CONVERGED</td> + * <td align="center">Camera device finished AE scan</td> + * <td align="center">FLASH_REQUIRED</td> + * <td align="center">Converged but too dark w/o flash after a new scan, transient states are skipped by camera device.</td> + * </tr> + * <tr> + * <td align="center">FLASH_REQUIRED</td> + * <td align="center">Camera device finished AE scan</td> + * <td align="center">CONVERGED</td> + * <td align="center">Converged after a new scan, transient states are skipped by camera device.</td> + * </tr> + * </tbody> + * </table> + * + * @see CaptureRequest#CONTROL_AE_LOCK + * @see CaptureRequest#CONTROL_AE_MODE + * @see CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER + * @see CaptureRequest#CONTROL_MODE + * @see CaptureRequest#CONTROL_SCENE_MODE * @see #CONTROL_AE_STATE_INACTIVE * @see #CONTROL_AE_STATE_SEARCHING * @see #CONTROL_AE_STATE_CONVERGED @@ -233,10 +435,17 @@ public final class CaptureResult extends CameraMetadata { new Key<Integer>("android.control.aeState", int.class); /** - * <p> - * Whether AF is currently enabled, and what - * mode it is set to - * </p> + * <p>Whether AF is currently enabled, and what + * mode it is set to</p> + * <p>Only effective if {@link CaptureRequest#CONTROL_MODE android.control.mode} = AUTO and the lens is not fixed focus + * (i.e. <code>{@link CameraCharacteristics#LENS_INFO_MINIMUM_FOCUS_DISTANCE android.lens.info.minimumFocusDistance} > 0</code>).</p> + * <p>If the lens is controlled by the camera device auto-focus algorithm, + * the camera device will report the current AF status in {@link CaptureResult#CONTROL_AF_STATE android.control.afState} + * in result metadata.</p> + * + * @see CaptureResult#CONTROL_AF_STATE + * @see CaptureRequest#CONTROL_MODE + * @see CameraCharacteristics#LENS_INFO_MINIMUM_FOCUS_DISTANCE * @see #CONTROL_AF_MODE_OFF * @see #CONTROL_AF_MODE_AUTO * @see #CONTROL_AF_MODE_MACRO @@ -248,41 +457,415 @@ public final class CaptureResult extends CameraMetadata { new Key<Integer>("android.control.afMode", int.class); /** - * <p> - * List of areas to use for focus - * estimation - * </p> - * <p> - * Each area is a rectangle plus weight: xmin, ymin, - * xmax, ymax, weight. The rectangle is defined inclusive of the - * specified coordinates. - * </p><p> - * The coordinate system is based on the active pixel array, + * <p>List of areas to use for focus + * estimation.</p> + * <p>Each area is a rectangle plus weight: xmin, ymin, + * xmax, ymax, weight. The rectangle is defined to be inclusive of the + * specified coordinates.</p> + * <p>The coordinate system is based on the active pixel array, * with (0,0) being the top-left pixel in the active pixel array, and - * (android.sensor.info.activeArraySize.width - 1, - * android.sensor.info.activeArraySize.height - 1) being the + * ({@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE android.sensor.info.activeArraySize}.width - 1, + * {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE android.sensor.info.activeArraySize}.height - 1) being the * bottom-right pixel in the active pixel array. The weight - * should be nonnegative. - * </p><p> - * If all regions have 0 weight, then no specific focus area - * needs to be used by the HAL. If the focusing region is - * outside the current android.scaler.cropRegion, the HAL - * should ignore the sections outside the region and output the - * used sections in the frame metadata - * </p> + * should be nonnegative.</p> + * <p>If all regions have 0 weight, then no specific focus area + * needs to be used by the camera device. If the focusing region is + * outside the current {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion}, the camera device + * will ignore the sections outside the region and output the + * used sections in the frame metadata.</p> + * + * @see CaptureRequest#SCALER_CROP_REGION + * @see CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE */ public static final Key<int[]> CONTROL_AF_REGIONS = new Key<int[]>("android.control.afRegions", int[].class); /** - * <p> - * Current state of AF algorithm - * </p> - * <p> - * Whenever the AF algorithm state changes, a - * MSG_AUTOFOCUS notification must be send if a notification - * callback is registered. - * </p> + * <p>Current state of AF algorithm.</p> + * <p>Switching between or enabling AF modes ({@link CaptureRequest#CONTROL_AF_MODE android.control.afMode}) always + * resets the AF state to INACTIVE. Similarly, switching between {@link CaptureRequest#CONTROL_MODE android.control.mode}, + * or {@link CaptureRequest#CONTROL_SCENE_MODE android.control.sceneMode} if <code>{@link CaptureRequest#CONTROL_MODE android.control.mode} == USE_SCENE_MODE</code> resets all + * the algorithm states to INACTIVE.</p> + * <p>The camera device can do several state transitions between two results, if it is + * allowed by the state transition table. For example: INACTIVE may never actually be + * seen in a result.</p> + * <p>The state in the result is the state for this image (in sync with this image): if + * AF state becomes FOCUSED, then the image data associated with this result should + * be sharp.</p> + * <p>Below are state transition tables for different AF modes.</p> + * <p>When {@link CaptureRequest#CONTROL_AF_MODE android.control.afMode} is AF_MODE_OFF or AF_MODE_EDOF:</p> + * <table> + * <thead> + * <tr> + * <th align="center">State</th> + * <th align="center">Transition Cause</th> + * <th align="center">New State</th> + * <th align="center">Notes</th> + * </tr> + * </thead> + * <tbody> + * <tr> + * <td align="center">INACTIVE</td> + * <td align="center"></td> + * <td align="center">INACTIVE</td> + * <td align="center">Never changes</td> + * </tr> + * </tbody> + * </table> + * <p>When {@link CaptureRequest#CONTROL_AF_MODE android.control.afMode} is AF_MODE_AUTO or AF_MODE_MACRO:</p> + * <table> + * <thead> + * <tr> + * <th align="center">State</th> + * <th align="center">Transition Cause</th> + * <th align="center">New State</th> + * <th align="center">Notes</th> + * </tr> + * </thead> + * <tbody> + * <tr> + * <td align="center">INACTIVE</td> + * <td align="center">AF_TRIGGER</td> + * <td align="center">ACTIVE_SCAN</td> + * <td align="center">Start AF sweep, Lens now moving</td> + * </tr> + * <tr> + * <td align="center">ACTIVE_SCAN</td> + * <td align="center">AF sweep done</td> + * <td align="center">FOCUSED_LOCKED</td> + * <td align="center">Focused, Lens now locked</td> + * </tr> + * <tr> + * <td align="center">ACTIVE_SCAN</td> + * <td align="center">AF sweep done</td> + * <td align="center">NOT_FOCUSED_LOCKED</td> + * <td align="center">Not focused, Lens now locked</td> + * </tr> + * <tr> + * <td align="center">ACTIVE_SCAN</td> + * <td align="center">AF_CANCEL</td> + * <td align="center">INACTIVE</td> + * <td align="center">Cancel/reset AF, Lens now locked</td> + * </tr> + * <tr> + * <td align="center">FOCUSED_LOCKED</td> + * <td align="center">AF_CANCEL</td> + * <td align="center">INACTIVE</td> + * <td align="center">Cancel/reset AF</td> + * </tr> + * <tr> + * <td align="center">FOCUSED_LOCKED</td> + * <td align="center">AF_TRIGGER</td> + * <td align="center">ACTIVE_SCAN</td> + * <td align="center">Start new sweep, Lens now moving</td> + * </tr> + * <tr> + * <td align="center">NOT_FOCUSED_LOCKED</td> + * <td align="center">AF_CANCEL</td> + * <td align="center">INACTIVE</td> + * <td align="center">Cancel/reset AF</td> + * </tr> + * <tr> + * <td align="center">NOT_FOCUSED_LOCKED</td> + * <td align="center">AF_TRIGGER</td> + * <td align="center">ACTIVE_SCAN</td> + * <td align="center">Start new sweep, Lens now moving</td> + * </tr> + * <tr> + * <td align="center">Any state</td> + * <td align="center">Mode change</td> + * <td align="center">INACTIVE</td> + * <td align="center"></td> + * </tr> + * </tbody> + * </table> + * <p>For the above table, the camera device may skip reporting any state changes that happen + * without application intervention (i.e. mode switch, trigger, locking). Any state that + * can be skipped in that manner is called a transient state.</p> + * <p>For example, for these AF modes (AF_MODE_AUTO and AF_MODE_MACRO), in addition to the + * state transitions listed in above table, it is also legal for the camera device to skip + * one or more transient states between two results. See below table for examples:</p> + * <table> + * <thead> + * <tr> + * <th align="center">State</th> + * <th align="center">Transition Cause</th> + * <th align="center">New State</th> + * <th align="center">Notes</th> + * </tr> + * </thead> + * <tbody> + * <tr> + * <td align="center">INACTIVE</td> + * <td align="center">AF_TRIGGER</td> + * <td align="center">FOCUSED_LOCKED</td> + * <td align="center">Focus is already good or good after a scan, lens is now locked.</td> + * </tr> + * <tr> + * <td align="center">INACTIVE</td> + * <td align="center">AF_TRIGGER</td> + * <td align="center">NOT_FOCUSED_LOCKED</td> + * <td align="center">Focus failed after a scan, lens is now locked.</td> + * </tr> + * <tr> + * <td align="center">FOCUSED_LOCKED</td> + * <td align="center">AF_TRIGGER</td> + * <td align="center">FOCUSED_LOCKED</td> + * <td align="center">Focus is already good or good after a scan, lens is now locked.</td> + * </tr> + * <tr> + * <td align="center">NOT_FOCUSED_LOCKED</td> + * <td align="center">AF_TRIGGER</td> + * <td align="center">FOCUSED_LOCKED</td> + * <td align="center">Focus is good after a scan, lens is not locked.</td> + * </tr> + * </tbody> + * </table> + * <p>When {@link CaptureRequest#CONTROL_AF_MODE android.control.afMode} is AF_MODE_CONTINUOUS_VIDEO:</p> + * <table> + * <thead> + * <tr> + * <th align="center">State</th> + * <th align="center">Transition Cause</th> + * <th align="center">New State</th> + * <th align="center">Notes</th> + * </tr> + * </thead> + * <tbody> + * <tr> + * <td align="center">INACTIVE</td> + * <td align="center">Camera device initiates new scan</td> + * <td align="center">PASSIVE_SCAN</td> + * <td align="center">Start AF scan, Lens now moving</td> + * </tr> + * <tr> + * <td align="center">INACTIVE</td> + * <td align="center">AF_TRIGGER</td> + * <td align="center">NOT_FOCUSED_LOCKED</td> + * <td align="center">AF state query, Lens now locked</td> + * </tr> + * <tr> + * <td align="center">PASSIVE_SCAN</td> + * <td align="center">Camera device completes current scan</td> + * <td align="center">PASSIVE_FOCUSED</td> + * <td align="center">End AF scan, Lens now locked</td> + * </tr> + * <tr> + * <td align="center">PASSIVE_SCAN</td> + * <td align="center">Camera device fails current scan</td> + * <td align="center">PASSIVE_UNFOCUSED</td> + * <td align="center">End AF scan, Lens now locked</td> + * </tr> + * <tr> + * <td align="center">PASSIVE_SCAN</td> + * <td align="center">AF_TRIGGER</td> + * <td align="center">FOCUSED_LOCKED</td> + * <td align="center">Immediate trans. If focus is good, Lens now locked</td> + * </tr> + * <tr> + * <td align="center">PASSIVE_SCAN</td> + * <td align="center">AF_TRIGGER</td> + * <td align="center">NOT_FOCUSED_LOCKED</td> + * <td align="center">Immediate trans. if focus is bad, Lens now locked</td> + * </tr> + * <tr> + * <td align="center">PASSIVE_SCAN</td> + * <td align="center">AF_CANCEL</td> + * <td align="center">INACTIVE</td> + * <td align="center">Reset lens position, Lens now locked</td> + * </tr> + * <tr> + * <td align="center">PASSIVE_FOCUSED</td> + * <td align="center">Camera device initiates new scan</td> + * <td align="center">PASSIVE_SCAN</td> + * <td align="center">Start AF scan, Lens now moving</td> + * </tr> + * <tr> + * <td align="center">PASSIVE_UNFOCUSED</td> + * <td align="center">Camera device initiates new scan</td> + * <td align="center">PASSIVE_SCAN</td> + * <td align="center">Start AF scan, Lens now moving</td> + * </tr> + * <tr> + * <td align="center">PASSIVE_FOCUSED</td> + * <td align="center">AF_TRIGGER</td> + * <td align="center">FOCUSED_LOCKED</td> + * <td align="center">Immediate trans. Lens now locked</td> + * </tr> + * <tr> + * <td align="center">PASSIVE_UNFOCUSED</td> + * <td align="center">AF_TRIGGER</td> + * <td align="center">NOT_FOCUSED_LOCKED</td> + * <td align="center">Immediate trans. Lens now locked</td> + * </tr> + * <tr> + * <td align="center">FOCUSED_LOCKED</td> + * <td align="center">AF_TRIGGER</td> + * <td align="center">FOCUSED_LOCKED</td> + * <td align="center">No effect</td> + * </tr> + * <tr> + * <td align="center">FOCUSED_LOCKED</td> + * <td align="center">AF_CANCEL</td> + * <td align="center">INACTIVE</td> + * <td align="center">Restart AF scan</td> + * </tr> + * <tr> + * <td align="center">NOT_FOCUSED_LOCKED</td> + * <td align="center">AF_TRIGGER</td> + * <td align="center">NOT_FOCUSED_LOCKED</td> + * <td align="center">No effect</td> + * </tr> + * <tr> + * <td align="center">NOT_FOCUSED_LOCKED</td> + * <td align="center">AF_CANCEL</td> + * <td align="center">INACTIVE</td> + * <td align="center">Restart AF scan</td> + * </tr> + * </tbody> + * </table> + * <p>When {@link CaptureRequest#CONTROL_AF_MODE android.control.afMode} is AF_MODE_CONTINUOUS_PICTURE:</p> + * <table> + * <thead> + * <tr> + * <th align="center">State</th> + * <th align="center">Transition Cause</th> + * <th align="center">New State</th> + * <th align="center">Notes</th> + * </tr> + * </thead> + * <tbody> + * <tr> + * <td align="center">INACTIVE</td> + * <td align="center">Camera device initiates new scan</td> + * <td align="center">PASSIVE_SCAN</td> + * <td align="center">Start AF scan, Lens now moving</td> + * </tr> + * <tr> + * <td align="center">INACTIVE</td> + * <td align="center">AF_TRIGGER</td> + * <td align="center">NOT_FOCUSED_LOCKED</td> + * <td align="center">AF state query, Lens now locked</td> + * </tr> + * <tr> + * <td align="center">PASSIVE_SCAN</td> + * <td align="center">Camera device completes current scan</td> + * <td align="center">PASSIVE_FOCUSED</td> + * <td align="center">End AF scan, Lens now locked</td> + * </tr> + * <tr> + * <td align="center">PASSIVE_SCAN</td> + * <td align="center">Camera device fails current scan</td> + * <td align="center">PASSIVE_UNFOCUSED</td> + * <td align="center">End AF scan, Lens now locked</td> + * </tr> + * <tr> + * <td align="center">PASSIVE_SCAN</td> + * <td align="center">AF_TRIGGER</td> + * <td align="center">FOCUSED_LOCKED</td> + * <td align="center">Eventual trans. once focus good, Lens now locked</td> + * </tr> + * <tr> + * <td align="center">PASSIVE_SCAN</td> + * <td align="center">AF_TRIGGER</td> + * <td align="center">NOT_FOCUSED_LOCKED</td> + * <td align="center">Eventual trans. if cannot focus, Lens now locked</td> + * </tr> + * <tr> + * <td align="center">PASSIVE_SCAN</td> + * <td align="center">AF_CANCEL</td> + * <td align="center">INACTIVE</td> + * <td align="center">Reset lens position, Lens now locked</td> + * </tr> + * <tr> + * <td align="center">PASSIVE_FOCUSED</td> + * <td align="center">Camera device initiates new scan</td> + * <td align="center">PASSIVE_SCAN</td> + * <td align="center">Start AF scan, Lens now moving</td> + * </tr> + * <tr> + * <td align="center">PASSIVE_UNFOCUSED</td> + * <td align="center">Camera device initiates new scan</td> + * <td align="center">PASSIVE_SCAN</td> + * <td align="center">Start AF scan, Lens now moving</td> + * </tr> + * <tr> + * <td align="center">PASSIVE_FOCUSED</td> + * <td align="center">AF_TRIGGER</td> + * <td align="center">FOCUSED_LOCKED</td> + * <td align="center">Immediate trans. Lens now locked</td> + * </tr> + * <tr> + * <td align="center">PASSIVE_UNFOCUSED</td> + * <td align="center">AF_TRIGGER</td> + * <td align="center">NOT_FOCUSED_LOCKED</td> + * <td align="center">Immediate trans. Lens now locked</td> + * </tr> + * <tr> + * <td align="center">FOCUSED_LOCKED</td> + * <td align="center">AF_TRIGGER</td> + * <td align="center">FOCUSED_LOCKED</td> + * <td align="center">No effect</td> + * </tr> + * <tr> + * <td align="center">FOCUSED_LOCKED</td> + * <td align="center">AF_CANCEL</td> + * <td align="center">INACTIVE</td> + * <td align="center">Restart AF scan</td> + * </tr> + * <tr> + * <td align="center">NOT_FOCUSED_LOCKED</td> + * <td align="center">AF_TRIGGER</td> + * <td align="center">NOT_FOCUSED_LOCKED</td> + * <td align="center">No effect</td> + * </tr> + * <tr> + * <td align="center">NOT_FOCUSED_LOCKED</td> + * <td align="center">AF_CANCEL</td> + * <td align="center">INACTIVE</td> + * <td align="center">Restart AF scan</td> + * </tr> + * </tbody> + * </table> + * <p>When switch between AF_MODE_CONTINUOUS_* (CAF modes) and AF_MODE_AUTO/AF_MODE_MACRO + * (AUTO modes), the initial INACTIVE or PASSIVE_SCAN states may be skipped by the + * camera device. When a trigger is included in a mode switch request, the trigger + * will be evaluated in the context of the new mode in the request. + * See below table for examples:</p> + * <table> + * <thead> + * <tr> + * <th align="center">State</th> + * <th align="center">Transition Cause</th> + * <th align="center">New State</th> + * <th align="center">Notes</th> + * </tr> + * </thead> + * <tbody> + * <tr> + * <td align="center">any state</td> + * <td align="center">CAF-->AUTO mode switch</td> + * <td align="center">INACTIVE</td> + * <td align="center">Mode switch without trigger, initial state must be INACTIVE</td> + * </tr> + * <tr> + * <td align="center">any state</td> + * <td align="center">CAF-->AUTO mode switch with AF_TRIGGER</td> + * <td align="center">trigger-reachable states from INACTIVE</td> + * <td align="center">Mode switch with trigger, INACTIVE is skipped</td> + * </tr> + * <tr> + * <td align="center">any state</td> + * <td align="center">AUTO-->CAF mode switch</td> + * <td align="center">passively reachable states from INACTIVE</td> + * <td align="center">Mode switch without trigger, passive transient state is skipped</td> + * </tr> + * </tbody> + * </table> + * + * @see CaptureRequest#CONTROL_AF_MODE + * @see CaptureRequest#CONTROL_MODE + * @see CaptureRequest#CONTROL_SCENE_MODE * @see #CONTROL_AF_STATE_INACTIVE * @see #CONTROL_AF_STATE_PASSIVE_SCAN * @see #CONTROL_AF_STATE_PASSIVE_FOCUSED @@ -295,30 +878,37 @@ public final class CaptureResult extends CameraMetadata { new Key<Integer>("android.control.afState", int.class); /** - * <p> - * The ID sent with the latest - * CAMERA2_TRIGGER_AUTOFOCUS call - * </p> - * <p> - * Must be 0 if no CAMERA2_TRIGGER_AUTOFOCUS trigger + * <p>The ID sent with the latest + * CAMERA2_TRIGGER_AUTOFOCUS call</p> + * <p>Must be 0 if no CAMERA2_TRIGGER_AUTOFOCUS trigger * received yet by HAL. Always updated even if AF algorithm - * ignores the trigger - * </p> - * + * ignores the trigger</p> * @hide */ public static final Key<Integer> CONTROL_AF_TRIGGER_ID = new Key<Integer>("android.control.afTriggerId", int.class); /** - * <p> - * Whether AWB is currently setting the color + * <p>Whether AWB is currently setting the color * transform fields, and what its illumination target - * is - * </p> - * <p> - * [BC - AWB lock,AWB modes] - * </p> + * is.</p> + * <p>This control is only effective if {@link CaptureRequest#CONTROL_MODE android.control.mode} is AUTO.</p> + * <p>When set to the ON mode, the camera device's auto white balance + * routine is enabled, overriding the application's selected + * {@link CaptureRequest#COLOR_CORRECTION_TRANSFORM android.colorCorrection.transform}, {@link CaptureRequest#COLOR_CORRECTION_GAINS android.colorCorrection.gains} and + * {@link CaptureRequest#COLOR_CORRECTION_MODE android.colorCorrection.mode}.</p> + * <p>When set to the OFF mode, the camera device's auto white balance + * routine is disabled. The application manually controls the white + * balance by {@link CaptureRequest#COLOR_CORRECTION_TRANSFORM android.colorCorrection.transform}, {@link CaptureRequest#COLOR_CORRECTION_GAINS android.colorCorrection.gains} + * and {@link CaptureRequest#COLOR_CORRECTION_MODE android.colorCorrection.mode}.</p> + * <p>When set to any other modes, the camera device's auto white balance + * routine is disabled. The camera device uses each particular illumination + * target for white balance adjustment.</p> + * + * @see CaptureRequest#COLOR_CORRECTION_GAINS + * @see CaptureRequest#COLOR_CORRECTION_MODE + * @see CaptureRequest#COLOR_CORRECTION_TRANSFORM + * @see CaptureRequest#CONTROL_MODE * @see #CONTROL_AWB_MODE_OFF * @see #CONTROL_AWB_MODE_AUTO * @see #CONTROL_AWB_MODE_INCANDESCENT @@ -333,43 +923,152 @@ public final class CaptureResult extends CameraMetadata { new Key<Integer>("android.control.awbMode", int.class); /** - * <p> - * List of areas to use for illuminant - * estimation - * </p> - * <p> - * Only used in AUTO mode. - * </p><p> - * Each area is a rectangle plus weight: xmin, ymin, - * xmax, ymax, weight. The rectangle is defined inclusive of the - * specified coordinates. - * </p><p> - * The coordinate system is based on the active pixel array, + * <p>List of areas to use for illuminant + * estimation.</p> + * <p>Only used in AUTO mode.</p> + * <p>Each area is a rectangle plus weight: xmin, ymin, + * xmax, ymax, weight. The rectangle is defined to be inclusive of the + * specified coordinates.</p> + * <p>The coordinate system is based on the active pixel array, * with (0,0) being the top-left pixel in the active pixel array, and - * (android.sensor.info.activeArraySize.width - 1, - * android.sensor.info.activeArraySize.height - 1) being the + * ({@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE android.sensor.info.activeArraySize}.width - 1, + * {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE android.sensor.info.activeArraySize}.height - 1) being the * bottom-right pixel in the active pixel array. The weight - * should be nonnegative. - * </p><p> - * If all regions have 0 weight, then no specific metering area - * needs to be used by the HAL. If the metering region is - * outside the current android.scaler.cropRegion, the HAL - * should ignore the sections outside the region and output the - * used sections in the frame metadata - * </p> + * should be nonnegative.</p> + * <p>If all regions have 0 weight, then no specific auto-white balance (AWB) area + * needs to be used by the camera device. If the AWB region is + * outside the current {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion}, the camera device + * will ignore the sections outside the region and output the + * used sections in the frame metadata.</p> + * + * @see CaptureRequest#SCALER_CROP_REGION + * @see CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE */ public static final Key<int[]> CONTROL_AWB_REGIONS = new Key<int[]>("android.control.awbRegions", int[].class); /** - * <p> - * Current state of AWB algorithm - * </p> - * <p> - * Whenever the AWB algorithm state changes, a - * MSG_AUTOWHITEBALANCE notification must be send if a - * notification callback is registered. - * </p> + * <p>Current state of AWB algorithm</p> + * <p>Switching between or enabling AWB modes ({@link CaptureRequest#CONTROL_AWB_MODE android.control.awbMode}) always + * resets the AWB state to INACTIVE. Similarly, switching between {@link CaptureRequest#CONTROL_MODE android.control.mode}, + * or {@link CaptureRequest#CONTROL_SCENE_MODE android.control.sceneMode} if <code>{@link CaptureRequest#CONTROL_MODE android.control.mode} == USE_SCENE_MODE</code> resets all + * the algorithm states to INACTIVE.</p> + * <p>The camera device can do several state transitions between two results, if it is + * allowed by the state transition table. So INACTIVE may never actually be seen in + * a result.</p> + * <p>The state in the result is the state for this image (in sync with this image): if + * AWB state becomes CONVERGED, then the image data associated with this result should + * be good to use.</p> + * <p>Below are state transition tables for different AWB modes.</p> + * <p>When <code>{@link CaptureRequest#CONTROL_AWB_MODE android.control.awbMode} != AWB_MODE_AUTO</code>:</p> + * <table> + * <thead> + * <tr> + * <th align="center">State</th> + * <th align="center">Transition Cause</th> + * <th align="center">New State</th> + * <th align="center">Notes</th> + * </tr> + * </thead> + * <tbody> + * <tr> + * <td align="center">INACTIVE</td> + * <td align="center"></td> + * <td align="center">INACTIVE</td> + * <td align="center">Camera device auto white balance algorithm is disabled</td> + * </tr> + * </tbody> + * </table> + * <p>When {@link CaptureRequest#CONTROL_AWB_MODE android.control.awbMode} is AWB_MODE_AUTO:</p> + * <table> + * <thead> + * <tr> + * <th align="center">State</th> + * <th align="center">Transition Cause</th> + * <th align="center">New State</th> + * <th align="center">Notes</th> + * </tr> + * </thead> + * <tbody> + * <tr> + * <td align="center">INACTIVE</td> + * <td align="center">Camera device initiates AWB scan</td> + * <td align="center">SEARCHING</td> + * <td align="center">Values changing</td> + * </tr> + * <tr> + * <td align="center">INACTIVE</td> + * <td align="center">{@link CaptureRequest#CONTROL_AWB_LOCK android.control.awbLock} is ON</td> + * <td align="center">LOCKED</td> + * <td align="center">Values locked</td> + * </tr> + * <tr> + * <td align="center">SEARCHING</td> + * <td align="center">Camera device finishes AWB scan</td> + * <td align="center">CONVERGED</td> + * <td align="center">Good values, not changing</td> + * </tr> + * <tr> + * <td align="center">SEARCHING</td> + * <td align="center">{@link CaptureRequest#CONTROL_AWB_LOCK android.control.awbLock} is ON</td> + * <td align="center">LOCKED</td> + * <td align="center">Values locked</td> + * </tr> + * <tr> + * <td align="center">CONVERGED</td> + * <td align="center">Camera device initiates AWB scan</td> + * <td align="center">SEARCHING</td> + * <td align="center">Values changing</td> + * </tr> + * <tr> + * <td align="center">CONVERGED</td> + * <td align="center">{@link CaptureRequest#CONTROL_AWB_LOCK android.control.awbLock} is ON</td> + * <td align="center">LOCKED</td> + * <td align="center">Values locked</td> + * </tr> + * <tr> + * <td align="center">LOCKED</td> + * <td align="center">{@link CaptureRequest#CONTROL_AWB_LOCK android.control.awbLock} is OFF</td> + * <td align="center">SEARCHING</td> + * <td align="center">Values not good after unlock</td> + * </tr> + * </tbody> + * </table> + * <p>For the above table, the camera device may skip reporting any state changes that happen + * without application intervention (i.e. mode switch, trigger, locking). Any state that + * can be skipped in that manner is called a transient state.</p> + * <p>For example, for this AWB mode (AWB_MODE_AUTO), in addition to the state transitions + * listed in above table, it is also legal for the camera device to skip one or more + * transient states between two results. See below table for examples:</p> + * <table> + * <thead> + * <tr> + * <th align="center">State</th> + * <th align="center">Transition Cause</th> + * <th align="center">New State</th> + * <th align="center">Notes</th> + * </tr> + * </thead> + * <tbody> + * <tr> + * <td align="center">INACTIVE</td> + * <td align="center">Camera device finished AWB scan</td> + * <td align="center">CONVERGED</td> + * <td align="center">Values are already good, transient states are skipped by camera device.</td> + * </tr> + * <tr> + * <td align="center">LOCKED</td> + * <td align="center">{@link CaptureRequest#CONTROL_AWB_LOCK android.control.awbLock} is OFF</td> + * <td align="center">CONVERGED</td> + * <td align="center">Values good after unlock, transient states are skipped by camera device.</td> + * </tr> + * </tbody> + * </table> + * + * @see CaptureRequest#CONTROL_AWB_LOCK + * @see CaptureRequest#CONTROL_AWB_MODE + * @see CaptureRequest#CONTROL_MODE + * @see CaptureRequest#CONTROL_SCENE_MODE * @see #CONTROL_AWB_STATE_INACTIVE * @see #CONTROL_AWB_STATE_SEARCHING * @see #CONTROL_AWB_STATE_CONVERGED @@ -379,22 +1078,46 @@ public final class CaptureResult extends CameraMetadata { new Key<Integer>("android.control.awbState", int.class); /** - * <p> - * Overall mode of 3A control - * routines - * </p> + * <p>Overall mode of 3A control + * routines.</p> + * <p>High-level 3A control. When set to OFF, all 3A control + * by the camera device is disabled. The application must set the fields for + * capture parameters itself.</p> + * <p>When set to AUTO, the individual algorithm controls in + * android.control.* are in effect, such as {@link CaptureRequest#CONTROL_AF_MODE android.control.afMode}.</p> + * <p>When set to USE_SCENE_MODE, the individual controls in + * android.control.* are mostly disabled, and the camera device implements + * one of the scene mode settings (such as ACTION, SUNSET, or PARTY) + * as it wishes. The camera device scene mode 3A settings are provided by + * android.control.sceneModeOverrides.</p> + * <p>When set to OFF_KEEP_STATE, it is similar to OFF mode, the only difference + * is that this frame will not be used by camera device background 3A statistics + * update, as if this frame is never captured. This mode can be used in the scenario + * where the application doesn't want a 3A manual control capture to affect + * the subsequent auto 3A capture results.</p> + * + * @see CaptureRequest#CONTROL_AF_MODE * @see #CONTROL_MODE_OFF * @see #CONTROL_MODE_AUTO * @see #CONTROL_MODE_USE_SCENE_MODE + * @see #CONTROL_MODE_OFF_KEEP_STATE */ public static final Key<Integer> CONTROL_MODE = new Key<Integer>("android.control.mode", int.class); /** - * <p> - * Operation mode for edge - * enhancement - * </p> + * <p>Operation mode for edge + * enhancement.</p> + * <p>Edge/sharpness/detail enhancement. OFF means no + * enhancement will be applied by the camera device.</p> + * <p>This must be set to one of the modes listed in {@link CameraCharacteristics#EDGE_AVAILABLE_EDGE_MODES android.edge.availableEdgeModes}.</p> + * <p>FAST/HIGH_QUALITY both mean camera device determined enhancement + * will be applied. HIGH_QUALITY mode indicates that the + * camera device will use the highest-quality enhancement algorithms, + * even if it slows down capture rate. FAST means the camera device will + * not slow down capture rate when applying edge enhancement.</p> + * + * @see CameraCharacteristics#EDGE_AVAILABLE_EDGE_MODES * @see #EDGE_MODE_OFF * @see #EDGE_MODE_FAST * @see #EDGE_MODE_HIGH_QUALITY @@ -403,9 +1126,25 @@ public final class CaptureResult extends CameraMetadata { new Key<Integer>("android.edge.mode", int.class); /** - * <p> - * Select flash operation mode - * </p> + * <p>The desired mode for for the camera device's flash control.</p> + * <p>This control is only effective when flash unit is available + * (<code>{@link CameraCharacteristics#FLASH_INFO_AVAILABLE android.flash.info.available} == true</code>).</p> + * <p>When this control is used, the {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} must be set to ON or OFF. + * Otherwise, the camera device auto-exposure related flash control (ON_AUTO_FLASH, + * ON_ALWAYS_FLASH, or ON_AUTO_FLASH_REDEYE) will override this control.</p> + * <p>When set to OFF, the camera device will not fire flash for this capture.</p> + * <p>When set to SINGLE, the camera device will fire flash regardless of the camera + * device's auto-exposure routine's result. When used in still capture case, this + * control should be used along with AE precapture metering sequence + * ({@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER android.control.aePrecaptureTrigger}), otherwise, the image may be incorrectly exposed.</p> + * <p>When set to TORCH, the flash will be on continuously. This mode can be used + * for use cases such as preview, auto-focus assist, still capture, or video recording.</p> + * <p>The flash status will be reported by {@link CaptureResult#FLASH_STATE android.flash.state} in the capture result metadata.</p> + * + * @see CaptureRequest#CONTROL_AE_MODE + * @see CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER + * @see CameraCharacteristics#FLASH_INFO_AVAILABLE + * @see CaptureResult#FLASH_STATE * @see #FLASH_MODE_OFF * @see #FLASH_MODE_SINGLE * @see #FLASH_MODE_TORCH @@ -414,153 +1153,188 @@ public final class CaptureResult extends CameraMetadata { new Key<Integer>("android.flash.mode", int.class); /** - * <p> - * Current state of the flash - * unit - * </p> + * <p>Current state of the flash + * unit.</p> + * <p>When the camera device doesn't have flash unit + * (i.e. <code>{@link CameraCharacteristics#FLASH_INFO_AVAILABLE android.flash.info.available} == false</code>), this state will always be UNAVAILABLE. + * Other states indicate the current flash status.</p> + * + * @see CameraCharacteristics#FLASH_INFO_AVAILABLE * @see #FLASH_STATE_UNAVAILABLE * @see #FLASH_STATE_CHARGING * @see #FLASH_STATE_READY * @see #FLASH_STATE_FIRED + * @see #FLASH_STATE_PARTIAL */ public static final Key<Integer> FLASH_STATE = new Key<Integer>("android.flash.state", int.class); /** - * <p> - * GPS coordinates to include in output JPEG - * EXIF - * </p> + * <p>Set operational mode for hot pixel correction.</p> + * <p>Valid modes for this camera device are listed in + * {@link CameraCharacteristics#HOT_PIXEL_AVAILABLE_HOT_PIXEL_MODES android.hotPixel.availableHotPixelModes}.</p> + * <p>Hotpixel correction interpolates out, or otherwise removes, pixels + * that do not accurately encode the incoming light (i.e. pixels that + * are stuck at an arbitrary value).</p> + * + * @see CameraCharacteristics#HOT_PIXEL_AVAILABLE_HOT_PIXEL_MODES + * @see #HOT_PIXEL_MODE_OFF + * @see #HOT_PIXEL_MODE_FAST + * @see #HOT_PIXEL_MODE_HIGH_QUALITY + */ + public static final Key<Integer> HOT_PIXEL_MODE = + new Key<Integer>("android.hotPixel.mode", int.class); + + /** + * <p>GPS coordinates to include in output JPEG + * EXIF</p> */ public static final Key<double[]> JPEG_GPS_COORDINATES = new Key<double[]>("android.jpeg.gpsCoordinates", double[].class); /** - * <p> - * 32 characters describing GPS algorithm to - * include in EXIF - * </p> + * <p>32 characters describing GPS algorithm to + * include in EXIF</p> */ public static final Key<String> JPEG_GPS_PROCESSING_METHOD = new Key<String>("android.jpeg.gpsProcessingMethod", String.class); /** - * <p> - * Time GPS fix was made to include in - * EXIF - * </p> + * <p>Time GPS fix was made to include in + * EXIF</p> */ public static final Key<Long> JPEG_GPS_TIMESTAMP = new Key<Long>("android.jpeg.gpsTimestamp", long.class); /** - * <p> - * Orientation of JPEG image to - * write - * </p> + * <p>Orientation of JPEG image to + * write</p> */ public static final Key<Integer> JPEG_ORIENTATION = new Key<Integer>("android.jpeg.orientation", int.class); /** - * <p> - * Compression quality of the final JPEG - * image - * </p> - * <p> - * 85-95 is typical usage range - * </p> + * <p>Compression quality of the final JPEG + * image</p> + * <p>85-95 is typical usage range</p> */ public static final Key<Byte> JPEG_QUALITY = new Key<Byte>("android.jpeg.quality", byte.class); /** - * <p> - * Compression quality of JPEG - * thumbnail - * </p> + * <p>Compression quality of JPEG + * thumbnail</p> */ public static final Key<Byte> JPEG_THUMBNAIL_QUALITY = new Key<Byte>("android.jpeg.thumbnailQuality", byte.class); /** - * <p> - * Resolution of embedded JPEG - * thumbnail - * </p> + * <p>Resolution of embedded JPEG thumbnail</p> + * <p>When set to (0, 0) value, the JPEG EXIF will not contain thumbnail, + * but the captured JPEG will still be a valid image.</p> + * <p>When a jpeg image capture is issued, the thumbnail size selected should have + * the same aspect ratio as the jpeg image.</p> */ public static final Key<android.hardware.camera2.Size> JPEG_THUMBNAIL_SIZE = new Key<android.hardware.camera2.Size>("android.jpeg.thumbnailSize", android.hardware.camera2.Size.class); /** - * <p> - * Size of the lens aperture - * </p> - * <p> - * Will not be supported on most devices. Can only - * pick from supported list - * </p> + * <p>The ratio of lens focal length to the effective + * aperture diameter.</p> + * <p>This will only be supported on the camera devices that + * have variable aperture lens. The aperture value can only be + * one of the values listed in {@link CameraCharacteristics#LENS_INFO_AVAILABLE_APERTURES android.lens.info.availableApertures}.</p> + * <p>When this is supported and {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} is OFF, + * this can be set along with {@link CaptureRequest#SENSOR_EXPOSURE_TIME android.sensor.exposureTime}, + * {@link CaptureRequest#SENSOR_SENSITIVITY android.sensor.sensitivity}, and {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration} + * to achieve manual exposure control.</p> + * <p>The requested aperture value may take several frames to reach the + * requested value; the camera device will report the current (intermediate) + * aperture size in capture result metadata while the aperture is changing. + * While the aperture is still changing, {@link CaptureResult#LENS_STATE android.lens.state} will be set to MOVING.</p> + * <p>When this is supported and {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} is one of + * the ON modes, this will be overridden by the camera device + * auto-exposure algorithm, the overridden values are then provided + * back to the user in the corresponding result.</p> + * + * @see CaptureRequest#CONTROL_AE_MODE + * @see CameraCharacteristics#LENS_INFO_AVAILABLE_APERTURES + * @see CaptureResult#LENS_STATE + * @see CaptureRequest#SENSOR_EXPOSURE_TIME + * @see CaptureRequest#SENSOR_FRAME_DURATION + * @see CaptureRequest#SENSOR_SENSITIVITY */ public static final Key<Float> LENS_APERTURE = new Key<Float>("android.lens.aperture", float.class); /** - * <p> - * State of lens neutral density - * filter(s) - * </p> - * <p> - * Will not be supported on most devices. Can only - * pick from supported list - * </p> + * <p>State of lens neutral density filter(s).</p> + * <p>This will not be supported on most camera devices. On devices + * where this is supported, this may only be set to one of the + * values included in {@link CameraCharacteristics#LENS_INFO_AVAILABLE_FILTER_DENSITIES android.lens.info.availableFilterDensities}.</p> + * <p>Lens filters are typically used to lower the amount of light the + * sensor is exposed to (measured in steps of EV). As used here, an EV + * step is the standard logarithmic representation, which are + * non-negative, and inversely proportional to the amount of light + * hitting the sensor. For example, setting this to 0 would result + * in no reduction of the incoming light, and setting this to 2 would + * mean that the filter is set to reduce incoming light by two stops + * (allowing 1/4 of the prior amount of light to the sensor).</p> + * <p>It may take several frames before the lens filter density changes + * to the requested value. While the filter density is still changing, + * {@link CaptureResult#LENS_STATE android.lens.state} will be set to MOVING.</p> + * + * @see CameraCharacteristics#LENS_INFO_AVAILABLE_FILTER_DENSITIES + * @see CaptureResult#LENS_STATE */ public static final Key<Float> LENS_FILTER_DENSITY = new Key<Float>("android.lens.filterDensity", float.class); /** - * <p> - * Lens optical zoom setting - * </p> - * <p> - * Will not be supported on most devices. - * </p> + * <p>The current lens focal length; used for optical zoom.</p> + * <p>This setting controls the physical focal length of the camera + * device's lens. Changing the focal length changes the field of + * view of the camera device, and is usually used for optical zoom.</p> + * <p>Like {@link CaptureRequest#LENS_FOCUS_DISTANCE android.lens.focusDistance} and {@link CaptureRequest#LENS_APERTURE android.lens.aperture}, this + * setting won't be applied instantaneously, and it may take several + * frames before the lens can change to the requested focal length. + * While the focal length is still changing, {@link CaptureResult#LENS_STATE android.lens.state} will + * be set to MOVING.</p> + * <p>This is expected not to be supported on most devices.</p> + * + * @see CaptureRequest#LENS_APERTURE + * @see CaptureRequest#LENS_FOCUS_DISTANCE + * @see CaptureResult#LENS_STATE */ public static final Key<Float> LENS_FOCAL_LENGTH = new Key<Float>("android.lens.focalLength", float.class); /** - * <p> - * Distance to plane of sharpest focus, - * measured from frontmost surface of the lens - * </p> - * <p> - * Should be zero for fixed-focus cameras - * </p> + * <p>Distance to plane of sharpest focus, + * measured from frontmost surface of the lens</p> + * <p>Should be zero for fixed-focus cameras</p> */ public static final Key<Float> LENS_FOCUS_DISTANCE = new Key<Float>("android.lens.focusDistance", float.class); /** - * <p> - * The range of scene distances that are in - * sharp focus (depth of field) - * </p> - * <p> - * If variable focus not supported, can still report - * fixed depth of field range - * </p> + * <p>The range of scene distances that are in + * sharp focus (depth of field)</p> + * <p>If variable focus not supported, can still report + * fixed depth of field range</p> */ public static final Key<float[]> LENS_FOCUS_RANGE = new Key<float[]>("android.lens.focusRange", float[].class); /** - * <p> - * Whether optical image stabilization is - * enabled. - * </p> - * <p> - * Will not be supported on most devices. - * </p> + * <p>Sets whether the camera device uses optical image stabilization (OIS) + * when capturing images.</p> + * <p>OIS is used to compensate for motion blur due to small movements of + * the camera during capture. Unlike digital image stabilization, OIS makes + * use of mechanical elements to stabilize the camera sensor, and thus + * allows for longer exposure times before camera shake becomes + * apparent.</p> + * <p>This is not expected to be supported on most devices.</p> * @see #LENS_OPTICAL_STABILIZATION_MODE_OFF * @see #LENS_OPTICAL_STABILIZATION_MODE_ON */ @@ -568,9 +1342,35 @@ public final class CaptureResult extends CameraMetadata { new Key<Integer>("android.lens.opticalStabilizationMode", int.class); /** - * <p> - * Current lens status - * </p> + * <p>Current lens status.</p> + * <p>For lens parameters {@link CaptureRequest#LENS_FOCAL_LENGTH android.lens.focalLength}, {@link CaptureRequest#LENS_FOCUS_DISTANCE android.lens.focusDistance}, + * {@link CaptureRequest#LENS_FILTER_DENSITY android.lens.filterDensity} and {@link CaptureRequest#LENS_APERTURE android.lens.aperture}, when changes are requested, + * they may take several frames to reach the requested values. This state indicates + * the current status of the lens parameters.</p> + * <p>When the state is STATIONARY, the lens parameters are not changing. This could be + * either because the parameters are all fixed, or because the lens has had enough + * time to reach the most recently-requested values. + * If all these lens parameters are not changable for a camera device, as listed below:</p> + * <ul> + * <li>Fixed focus (<code>{@link CameraCharacteristics#LENS_INFO_MINIMUM_FOCUS_DISTANCE android.lens.info.minimumFocusDistance} == 0</code>), which means + * {@link CaptureRequest#LENS_FOCUS_DISTANCE android.lens.focusDistance} parameter will always be 0.</li> + * <li>Fixed focal length ({@link CameraCharacteristics#LENS_INFO_AVAILABLE_FOCAL_LENGTHS android.lens.info.availableFocalLengths} contains single value), + * which means the optical zoom is not supported.</li> + * <li>No ND filter ({@link CameraCharacteristics#LENS_INFO_AVAILABLE_FILTER_DENSITIES android.lens.info.availableFilterDensities} contains only 0).</li> + * <li>Fixed aperture ({@link CameraCharacteristics#LENS_INFO_AVAILABLE_APERTURES android.lens.info.availableApertures} contains single value).</li> + * </ul> + * <p>Then this state will always be STATIONARY.</p> + * <p>When the state is MOVING, it indicates that at least one of the lens parameters + * is changing.</p> + * + * @see CaptureRequest#LENS_APERTURE + * @see CaptureRequest#LENS_FILTER_DENSITY + * @see CaptureRequest#LENS_FOCAL_LENGTH + * @see CaptureRequest#LENS_FOCUS_DISTANCE + * @see CameraCharacteristics#LENS_INFO_AVAILABLE_APERTURES + * @see CameraCharacteristics#LENS_INFO_AVAILABLE_FILTER_DENSITIES + * @see CameraCharacteristics#LENS_INFO_AVAILABLE_FOCAL_LENGTHS + * @see CameraCharacteristics#LENS_INFO_MINIMUM_FOCUS_DISTANCE * @see #LENS_STATE_STATIONARY * @see #LENS_STATE_MOVING */ @@ -578,10 +1378,19 @@ public final class CaptureResult extends CameraMetadata { new Key<Integer>("android.lens.state", int.class); /** - * <p> - * Mode of operation for the noise reduction - * algorithm - * </p> + * <p>Mode of operation for the noise reduction + * algorithm</p> + * <p>Noise filtering control. OFF means no noise reduction + * will be applied by the camera device.</p> + * <p>This must be set to a valid mode in + * {@link CameraCharacteristics#NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES android.noiseReduction.availableNoiseReductionModes}.</p> + * <p>FAST/HIGH_QUALITY both mean camera device determined noise filtering + * will be applied. HIGH_QUALITY mode indicates that the camera device + * will use the highest-quality noise filtering algorithms, + * even if it slows down capture rate. FAST means the camera device should not + * slow down capture rate when applying noise filtering.</p> + * + * @see CameraCharacteristics#NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES * @see #NOISE_REDUCTION_MODE_OFF * @see #NOISE_REDUCTION_MODE_FAST * @see #NOISE_REDUCTION_MODE_HIGH_QUALITY @@ -590,14 +1399,11 @@ public final class CaptureResult extends CameraMetadata { new Key<Integer>("android.noiseReduction.mode", int.class); /** - * <p> - * Whether a result given to the framework is the + * <p>Whether a result given to the framework is the * final one for the capture, or only a partial that contains a * subset of the full set of dynamic metadata - * values. - * </p> - * <p> - * The entries in the result metadata buffers for a + * values.</p> + * <p>The entries in the result metadata buffers for a * single capture may not overlap, except for this entry. The * FINAL buffers must retain FIFO ordering relative to the * requests that generate them, so the FINAL buffer for frame 3 must @@ -605,68 +1411,64 @@ public final class CaptureResult extends CameraMetadata { * before the FINAL buffer for frame 4. PARTIAL buffers may be returned * in any order relative to other frames, but all PARTIAL buffers for a given * capture must arrive before the FINAL buffer for that capture. This entry may - * only be used by the HAL if quirks.usePartialResult is set to 1. - * </p> - * - * <b>Optional</b> - This value may be null on some devices. - * + * only be used by the camera device if quirks.usePartialResult is set to 1.</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> * @hide */ public static final Key<Boolean> QUIRKS_PARTIAL_RESULT = new Key<Boolean>("android.quirks.partialResult", boolean.class); /** - * <p> - * A frame counter set by the framework. This value monotonically + * <p>A frame counter set by the framework. This value monotonically * increases with every new result (that is, each new result has a unique - * frameCount value). - * </p> - * <p> - * Reset on release() - * </p> + * frameCount value).</p> + * <p>Reset on release()</p> */ public static final Key<Integer> REQUEST_FRAME_COUNT = new Key<Integer>("android.request.frameCount", int.class); /** - * <p> - * An application-specified ID for the current + * <p>An application-specified ID for the current * request. Must be maintained unchanged in output - * frame - * </p> - * + * frame</p> * @hide */ public static final Key<Integer> REQUEST_ID = new Key<Integer>("android.request.id", int.class); /** - * <p> - * (x, y, width, height). - * </p><p> - * A rectangle with the top-level corner of (x,y) and size + * <p>Specifies the number of pipeline stages the frame went + * through from when it was exposed to when the final completed result + * was available to the framework.</p> + * <p>Depending on what settings are used in the request, and + * what streams are configured, the data may undergo less processing, + * and some pipeline stages skipped.</p> + * <p>See {@link CameraCharacteristics#REQUEST_PIPELINE_MAX_DEPTH android.request.pipelineMaxDepth} for more details.</p> + * + * @see CameraCharacteristics#REQUEST_PIPELINE_MAX_DEPTH + */ + public static final Key<Byte> REQUEST_PIPELINE_DEPTH = + new Key<Byte>("android.request.pipelineDepth", byte.class); + + /** + * <p>(x, y, width, height).</p> + * <p>A rectangle with the top-level corner of (x,y) and size * (width, height). The region of the sensor that is used for * output. Each stream must use this rectangle to produce its * output, cropping to a smaller region if necessary to - * maintain the stream's aspect ratio. - * </p><p> - * HAL2.x uses only (x, y, width) - * </p> - * <p> - * Any additional per-stream cropping must be done to - * maximize the final pixel area of the stream. - * </p><p> - * For example, if the crop region is set to a 4:3 aspect + * maintain the stream's aspect ratio.</p> + * <p>HAL2.x uses only (x, y, width)</p> + * <p>Any additional per-stream cropping must be done to + * maximize the final pixel area of the stream.</p> + * <p>For example, if the crop region is set to a 4:3 aspect * ratio, then 4:3 streams should use the exact crop * region. 16:9 streams should further crop vertically - * (letterbox). - * </p><p> - * Conversely, if the crop region is set to a 16:9, then 4:3 + * (letterbox).</p> + * <p>Conversely, if the crop region is set to a 16:9, then 4:3 * outputs should crop horizontally (pillarbox), and 16:9 * streams should match exactly. These additional crops must - * be centered within the crop region. - * </p><p> - * The output streams must maintain square pixels at all + * be centered within the crop region.</p> + * <p>The output streams must maintain square pixels at all * times, no matter what the relative aspect ratios of the * crop region and the stream are. Negative values for * corner are allowed for raw output if full pixel array is @@ -675,100 +1477,340 @@ public final class CaptureResult extends CameraMetadata { * for raw output, where only a few fixed scales may be * possible. The width and height of the crop region cannot * be set to be smaller than floor( activeArraySize.width / - * android.scaler.maxDigitalZoom ) and floor( - * activeArraySize.height / android.scaler.maxDigitalZoom), - * respectively. - * </p> + * {@link CameraCharacteristics#SCALER_AVAILABLE_MAX_DIGITAL_ZOOM android.scaler.availableMaxDigitalZoom} ) and floor( + * activeArraySize.height / + * {@link CameraCharacteristics#SCALER_AVAILABLE_MAX_DIGITAL_ZOOM android.scaler.availableMaxDigitalZoom}), respectively.</p> + * + * @see CameraCharacteristics#SCALER_AVAILABLE_MAX_DIGITAL_ZOOM */ public static final Key<android.graphics.Rect> SCALER_CROP_REGION = new Key<android.graphics.Rect>("android.scaler.cropRegion", android.graphics.Rect.class); /** - * <p> - * Duration each pixel is exposed to - * light. - * </p><p> - * If the sensor can't expose this exact duration, it should shorten the - * duration exposed to the nearest possible value (rather than expose longer). - * </p> - * <p> - * 1/10000 - 30 sec range. No bulb mode - * </p> + * <p>Duration each pixel is exposed to + * light.</p> + * <p>If the sensor can't expose this exact duration, it should shorten the + * duration exposed to the nearest possible value (rather than expose longer).</p> */ public static final Key<Long> SENSOR_EXPOSURE_TIME = new Key<Long>("android.sensor.exposureTime", long.class); /** - * <p> - * Duration from start of frame exposure to - * start of next frame exposure - * </p> - * <p> - * Exposure time has priority, so duration is set to - * max(duration, exposure time + overhead) - * </p> + * <p>Duration from start of frame exposure to + * start of next frame exposure.</p> + * <p>The maximum frame rate that can be supported by a camera subsystem is + * a function of many factors:</p> + * <ul> + * <li>Requested resolutions of output image streams</li> + * <li>Availability of binning / skipping modes on the imager</li> + * <li>The bandwidth of the imager interface</li> + * <li>The bandwidth of the various ISP processing blocks</li> + * </ul> + * <p>Since these factors can vary greatly between different ISPs and + * sensors, the camera abstraction tries to represent the bandwidth + * restrictions with as simple a model as possible.</p> + * <p>The model presented has the following characteristics:</p> + * <ul> + * <li>The image sensor is always configured to output the smallest + * resolution possible given the application's requested output stream + * sizes. The smallest resolution is defined as being at least as large + * as the largest requested output stream size; the camera pipeline must + * never digitally upsample sensor data when the crop region covers the + * whole sensor. In general, this means that if only small output stream + * resolutions are configured, the sensor can provide a higher frame + * rate.</li> + * <li>Since any request may use any or all the currently configured + * output streams, the sensor and ISP must be configured to support + * scaling a single capture to all the streams at the same time. This + * means the camera pipeline must be ready to produce the largest + * requested output size without any delay. Therefore, the overall + * frame rate of a given configured stream set is governed only by the + * largest requested stream resolution.</li> + * <li>Using more than one output stream in a request does not affect the + * frame duration.</li> + * <li>Certain format-streams may need to do additional background processing + * before data is consumed/produced by that stream. These processors + * can run concurrently to the rest of the camera pipeline, but + * cannot process more than 1 capture at a time.</li> + * </ul> + * <p>The necessary information for the application, given the model above, + * is provided via the {@link CameraCharacteristics#SCALER_AVAILABLE_MIN_FRAME_DURATIONS android.scaler.availableMinFrameDurations} field. + * These are used to determine the maximum frame rate / minimum frame + * duration that is possible for a given stream configuration.</p> + * <p>Specifically, the application can use the following rules to + * determine the minimum frame duration it can request from the camera + * device:</p> + * <ol> + * <li>Let the set of currently configured input/output streams + * be called <code>S</code>.</li> + * <li>Find the minimum frame durations for each stream in <code>S</code>, by + * looking it up in {@link CameraCharacteristics#SCALER_AVAILABLE_MIN_FRAME_DURATIONS android.scaler.availableMinFrameDurations} (with + * its respective size/format). Let this set of frame durations be called + * <code>F</code>.</li> + * <li>For any given request <code>R</code>, the minimum frame duration allowed + * for <code>R</code> is the maximum out of all values in <code>F</code>. Let the streams + * used in <code>R</code> be called <code>S_r</code>.</li> + * </ol> + * <p>If none of the streams in <code>S_r</code> have a stall time (listed in + * {@link CameraCharacteristics#SCALER_AVAILABLE_STALL_DURATIONS android.scaler.availableStallDurations}), then the frame duration in + * <code>F</code> determines the steady state frame rate that the application will + * get if it uses <code>R</code> as a repeating request. Let this special kind + * of request be called <code>Rsimple</code>.</p> + * <p>A repeating request <code>Rsimple</code> can be <em>occasionally</em> interleaved + * by a single capture of a new request <code>Rstall</code> (which has at least + * one in-use stream with a non-0 stall time) and if <code>Rstall</code> has the + * same minimum frame duration this will not cause a frame rate loss + * if all buffers from the previous <code>Rstall</code> have already been + * delivered.</p> + * <p>For more details about stalling, see + * {@link CameraCharacteristics#SCALER_AVAILABLE_STALL_DURATIONS android.scaler.availableStallDurations}.</p> + * + * @see CameraCharacteristics#SCALER_AVAILABLE_MIN_FRAME_DURATIONS + * @see CameraCharacteristics#SCALER_AVAILABLE_STALL_DURATIONS */ public static final Key<Long> SENSOR_FRAME_DURATION = new Key<Long>("android.sensor.frameDuration", long.class); /** - * <p> - * Gain applied to image data. Must be + * <p>Gain applied to image data. Must be * implemented through analog gain only if set to values - * below 'maximum analog sensitivity'. - * </p><p> - * If the sensor can't apply this exact gain, it should lessen the - * gain to the nearest possible value (rather than gain more). - * </p> - * <p> - * ISO 12232:2006 REI method - * </p> + * below 'maximum analog sensitivity'.</p> + * <p>If the sensor can't apply this exact gain, it should lessen the + * gain to the nearest possible value (rather than gain more).</p> + * <p>ISO 12232:2006 REI method</p> */ public static final Key<Integer> SENSOR_SENSITIVITY = new Key<Integer>("android.sensor.sensitivity", int.class); /** - * <p> - * Time at start of exposure of first - * row - * </p> - * <p> - * Monotonic, should be synced to other timestamps in - * system - * </p> + * <p>Time at start of exposure of first + * row</p> + * <p>Monotonic, should be synced to other timestamps in + * system</p> */ public static final Key<Long> SENSOR_TIMESTAMP = new Key<Long>("android.sensor.timestamp", long.class); /** - * <p> - * The temperature of the sensor, sampled at the time - * exposure began for this frame. - * </p><p> - * The thermal diode being queried should be inside the sensor PCB, or - * somewhere close to it. - * </p> - * - * <b>Optional</b> - This value may be null on some devices. + * <p>The temperature of the sensor, sampled at the time + * exposure began for this frame.</p> + * <p>The thermal diode being queried should be inside the sensor PCB, or + * somewhere close to it.</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + * <p><b>Full capability</b> - + * Present on all camera devices that report being {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_FULL HARDWARE_LEVEL_FULL} devices in the + * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p> * - * <b>{@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_FULL HARDWARE_LEVEL_FULL}</b> - - * Present on all devices that report being FULL level hardware devices in the - * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL HARDWARE_LEVEL} key. + * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL */ public static final Key<Float> SENSOR_TEMPERATURE = new Key<Float>("android.sensor.temperature", float.class); /** - * <p> - * State of the face detector - * unit - * </p> - * <p> - * Whether face detection is enabled, and whether it + * <p>A reference illumination source roughly matching the current scene + * illumination, which is used to describe the sensor color space + * transformations.</p> + * <p>The values in this tag correspond to the values defined for the + * EXIF LightSource tag. These illuminants are standard light sources + * that are often used for calibrating camera devices.</p> + * @see #SENSOR_REFERENCE_ILLUMINANT_DAYLIGHT + * @see #SENSOR_REFERENCE_ILLUMINANT_FLUORESCENT + * @see #SENSOR_REFERENCE_ILLUMINANT_TUNGSTEN + * @see #SENSOR_REFERENCE_ILLUMINANT_FLASH + * @see #SENSOR_REFERENCE_ILLUMINANT_FINE_WEATHER + * @see #SENSOR_REFERENCE_ILLUMINANT_CLOUDY_WEATHER + * @see #SENSOR_REFERENCE_ILLUMINANT_SHADE + * @see #SENSOR_REFERENCE_ILLUMINANT_DAYLIGHT_FLUORESCENT + * @see #SENSOR_REFERENCE_ILLUMINANT_DAY_WHITE_FLUORESCENT + * @see #SENSOR_REFERENCE_ILLUMINANT_COOL_WHITE_FLUORESCENT + * @see #SENSOR_REFERENCE_ILLUMINANT_WHITE_FLUORESCENT + * @see #SENSOR_REFERENCE_ILLUMINANT_STANDARD_A + * @see #SENSOR_REFERENCE_ILLUMINANT_STANDARD_B + * @see #SENSOR_REFERENCE_ILLUMINANT_STANDARD_C + * @see #SENSOR_REFERENCE_ILLUMINANT_D55 + * @see #SENSOR_REFERENCE_ILLUMINANT_D65 + * @see #SENSOR_REFERENCE_ILLUMINANT_D75 + * @see #SENSOR_REFERENCE_ILLUMINANT_D50 + * @see #SENSOR_REFERENCE_ILLUMINANT_ISO_STUDIO_TUNGSTEN + */ + public static final Key<Integer> SENSOR_REFERENCE_ILLUMINANT = + new Key<Integer>("android.sensor.referenceIlluminant", int.class); + + /** + * <p>A per-device calibration transform matrix to be applied after the + * color space transform when rendering the raw image buffer.</p> + * <p>This matrix is expressed as a 3x3 matrix in row-major-order, and + * contains a per-device calibration transform that maps colors + * from reference camera color space (i.e. the "golden module" + * colorspace) into this camera device's linear native sensor color + * space for the current scene illumination and white balance choice.</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + */ + public static final Key<Rational[]> SENSOR_CALIBRATION_TRANSFORM = + new Key<Rational[]>("android.sensor.calibrationTransform", Rational[].class); + + /** + * <p>A matrix that transforms color values from CIE XYZ color space to + * reference camera color space when rendering the raw image buffer.</p> + * <p>This matrix is expressed as a 3x3 matrix in row-major-order, and + * contains a color transform matrix that maps colors from the CIE + * XYZ color space to the reference camera raw color space (i.e. the + * "golden module" colorspace) for the current scene illumination and + * white balance choice.</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + */ + public static final Key<Rational[]> SENSOR_COLOR_TRANSFORM = + new Key<Rational[]>("android.sensor.colorTransform", Rational[].class); + + /** + * <p>A matrix that transforms white balanced camera colors to the CIE XYZ + * colorspace with a D50 whitepoint.</p> + * <p>This matrix is expressed as a 3x3 matrix in row-major-order, and contains + * a color transform matrix that maps a unit vector in the linear native + * sensor color space to the D50 whitepoint in CIE XYZ color space.</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + */ + public static final Key<Rational[]> SENSOR_FORWARD_MATRIX = + new Key<Rational[]>("android.sensor.forwardMatrix", Rational[].class); + + /** + * <p>The estimated white balance at the time of capture.</p> + * <p>The estimated white balance encoded as the RGB values of the + * perfectly neutral color point in the linear native sensor color space. + * The order of the values is R, G, B; where R is in the lowest index.</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + */ + public static final Key<Rational[]> SENSOR_NEUTRAL_COLOR_POINT = + new Key<Rational[]>("android.sensor.neutralColorPoint", Rational[].class); + + /** + * <p>A mapping containing a hue shift, saturation scale, and value scale + * for each pixel.</p> + * <p>hue_samples, saturation_samples, and value_samples are given in + * {@link CameraCharacteristics#SENSOR_PROFILE_HUE_SAT_MAP_DIMENSIONS android.sensor.profileHueSatMapDimensions}.</p> + * <p>Each entry of this map contains three floats corresponding to the + * hue shift, saturation scale, and value scale, respectively; where the + * hue shift has the lowest index. The map entries are stored in the tag + * in nested loop order, with the value divisions in the outer loop, the + * hue divisions in the middle loop, and the saturation divisions in the + * inner loop. All zero input saturation entries are required to have a + * value scale factor of 1.0.</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + * + * @see CameraCharacteristics#SENSOR_PROFILE_HUE_SAT_MAP_DIMENSIONS + */ + public static final Key<float[]> SENSOR_PROFILE_HUE_SAT_MAP = + new Key<float[]>("android.sensor.profileHueSatMap", float[].class); + + /** + * <p>A list of x,y samples defining a tone-mapping curve for gamma adjustment.</p> + * <p>This tag contains a default tone curve that can be applied while + * processing the image as a starting point for user adjustments. + * The curve is specified as a list of value pairs in linear gamma. + * The curve is interpolated using a cubic spline.</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + */ + public static final Key<float[]> SENSOR_PROFILE_TONE_CURVE = + new Key<float[]>("android.sensor.profileToneCurve", float[].class); + + /** + * <p>The worst-case divergence between Bayer green channels.</p> + * <p>This value is an estimate of the worst case split between the + * Bayer green channels in the red and blue rows in the sensor color + * filter array.</p> + * <p>The green split is calculated as follows:</p> + * <ol> + * <li>A 5x5 pixel (or larger) window W within the active sensor array is + * chosen. The term 'pixel' here is taken to mean a group of 4 Bayer + * mosaic channels (R, Gr, Gb, B). The location and size of the window + * chosen is implementation defined, and should be chosen to provide a + * green split estimate that is both representative of the entire image + * for this camera sensor, and can be calculated quickly.</li> + * <li>The arithmetic mean of the green channels from the red + * rows (mean_Gr) within W is computed.</li> + * <li>The arithmetic mean of the green channels from the blue + * rows (mean_Gb) within W is computed.</li> + * <li>The maximum ratio R of the two means is computed as follows: + * <code>R = max((mean_Gr + 1)/(mean_Gb + 1), (mean_Gb + 1)/(mean_Gr + 1))</code></li> + * </ol> + * <p>The ratio R is the green split divergence reported for this property, + * which represents how much the green channels differ in the mosaic + * pattern. This value is typically used to determine the treatment of + * the green mosaic channels when demosaicing.</p> + * <p>The green split value can be roughly interpreted as follows:</p> + * <ul> + * <li>R < 1.03 is a negligible split (<3% divergence).</li> + * <li>1.20 <= R >= 1.03 will require some software + * correction to avoid demosaic errors (3-20% divergence).</li> + * <li>R > 1.20 will require strong software correction to produce + * a usuable image (>20% divergence).</li> + * </ul> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + */ + public static final Key<Float> SENSOR_GREEN_SPLIT = + new Key<Float>("android.sensor.greenSplit", float.class); + + /** + * <p>When enabled, the sensor sends a test pattern instead of + * doing a real exposure from the camera.</p> + * <p>When a test pattern is enabled, all manual sensor controls specified + * by android.sensor.* should be ignored. All other controls should + * work as normal.</p> + * <p>For example, if manual flash is enabled, flash firing should still + * occur (and that the test pattern remain unmodified, since the flash + * would not actually affect it).</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + * @see #SENSOR_TEST_PATTERN_MODE_OFF + * @see #SENSOR_TEST_PATTERN_MODE_SOLID_COLOR + * @see #SENSOR_TEST_PATTERN_MODE_COLOR_BARS + * @see #SENSOR_TEST_PATTERN_MODE_COLOR_BARS_FADE_TO_GRAY + * @see #SENSOR_TEST_PATTERN_MODE_PN9 + * @see #SENSOR_TEST_PATTERN_MODE_CUSTOM1 + */ + public static final Key<Integer> SENSOR_TEST_PATTERN_MODE = + new Key<Integer>("android.sensor.testPatternMode", int.class); + + /** + * <p>Quality of lens shading correction applied + * to the image data.</p> + * <p>When set to OFF mode, no lens shading correction will be applied by the + * camera device, and an identity lens shading map data will be provided + * if <code>{@link CaptureRequest#STATISTICS_LENS_SHADING_MAP_MODE android.statistics.lensShadingMapMode} == ON</code>. For example, for lens + * shading map with size specified as <code>{@link CameraCharacteristics#LENS_INFO_SHADING_MAP_SIZE android.lens.info.shadingMapSize} = [ 4, 3 ]</code>, + * the output {@link CaptureResult#STATISTICS_LENS_SHADING_MAP android.statistics.lensShadingMap} for this case will be an identity map + * shown below:</p> + * <pre><code>[ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + * 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + * 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + * 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + * 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + * 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 ] + * </code></pre> + * <p>When set to other modes, lens shading correction will be applied by the + * camera device. Applications can request lens shading map data by setting + * {@link CaptureRequest#STATISTICS_LENS_SHADING_MAP_MODE android.statistics.lensShadingMapMode} to ON, and then the camera device will provide + * lens shading map data in {@link CaptureResult#STATISTICS_LENS_SHADING_MAP android.statistics.lensShadingMap}, with size specified + * by {@link CameraCharacteristics#LENS_INFO_SHADING_MAP_SIZE android.lens.info.shadingMapSize}.</p> + * + * @see CameraCharacteristics#LENS_INFO_SHADING_MAP_SIZE + * @see CaptureResult#STATISTICS_LENS_SHADING_MAP + * @see CaptureRequest#STATISTICS_LENS_SHADING_MAP_MODE + * @see #SHADING_MODE_OFF + * @see #SHADING_MODE_FAST + * @see #SHADING_MODE_HIGH_QUALITY + */ + public static final Key<Integer> SHADING_MODE = + new Key<Integer>("android.shading.mode", int.class); + + /** + * <p>State of the face detector + * unit</p> + * <p>Whether face detection is enabled, and whether it * should output just the basic fields or the full set of * fields. Value must be one of the - * android.statistics.info.availableFaceDetectModes. - * </p> + * {@link CameraCharacteristics#STATISTICS_INFO_AVAILABLE_FACE_DETECT_MODES android.statistics.info.availableFaceDetectModes}.</p> + * + * @see CameraCharacteristics#STATISTICS_INFO_AVAILABLE_FACE_DETECT_MODES * @see #STATISTICS_FACE_DETECT_MODE_OFF * @see #STATISTICS_FACE_DETECT_MODE_SIMPLE * @see #STATISTICS_FACE_DETECT_MODE_FULL @@ -777,129 +1819,151 @@ public final class CaptureResult extends CameraMetadata { new Key<Integer>("android.statistics.faceDetectMode", int.class); /** - * <p> - * List of unique IDs for detected - * faces - * </p> - * <p> - * Only available if faceDetectMode == FULL - * </p> + * <p>List of unique IDs for detected + * faces</p> + * <p>Only available if faceDetectMode == FULL</p> + * @hide */ public static final Key<int[]> STATISTICS_FACE_IDS = new Key<int[]>("android.statistics.faceIds", int[].class); /** - * <p> - * List of landmarks for detected - * faces - * </p> - * <p> - * Only available if faceDetectMode == FULL - * </p> + * <p>List of landmarks for detected + * faces</p> + * <p>Only available if faceDetectMode == FULL</p> + * @hide */ public static final Key<int[]> STATISTICS_FACE_LANDMARKS = new Key<int[]>("android.statistics.faceLandmarks", int[].class); /** - * <p> - * List of the bounding rectangles for detected - * faces - * </p> - * <p> - * Only available if faceDetectMode != OFF - * </p> + * <p>List of the bounding rectangles for detected + * faces</p> + * <p>Only available if faceDetectMode != OFF</p> + * @hide */ public static final Key<android.graphics.Rect[]> STATISTICS_FACE_RECTANGLES = new Key<android.graphics.Rect[]>("android.statistics.faceRectangles", android.graphics.Rect[].class); /** - * <p> - * List of the face confidence scores for - * detected faces - * </p> - * <p> - * Only available if faceDetectMode != OFF. The value should be - * meaningful (for example, setting 100 at all times is illegal). - * </p> + * <p>List of the face confidence scores for + * detected faces</p> + * <p>Only available if faceDetectMode != OFF. The value should be + * meaningful (for example, setting 100 at all times is illegal).</p> + * @hide */ public static final Key<byte[]> STATISTICS_FACE_SCORES = new Key<byte[]>("android.statistics.faceScores", byte[].class); /** - * <p> - * A low-resolution map of lens shading, per - * color channel - * </p> - * <p> - * Assume bilinear interpolation of map. The least - * shaded section of the image should have a gain factor - * of 1; all other sections should have gains above 1. - * the map should be on the order of 30-40 rows, and - * must be smaller than 64x64. - * </p><p> - * When android.colorCorrection.mode = TRANSFORM_MATRIX, the map - * must take into account the colorCorrection settings. - * </p> + * <p>The shading map is a low-resolution floating-point map + * that lists the coefficients used to correct for vignetting, for each + * Bayer color channel.</p> + * <p>The least shaded section of the image should have a gain factor + * of 1; all other sections should have gains above 1.</p> + * <p>When {@link CaptureRequest#COLOR_CORRECTION_MODE android.colorCorrection.mode} = TRANSFORM_MATRIX, the map + * must take into account the colorCorrection settings.</p> + * <p>The shading map is for the entire active pixel array, and is not + * affected by the crop region specified in the request. Each shading map + * entry is the value of the shading compensation map over a specific + * pixel on the sensor. Specifically, with a (N x M) resolution shading + * map, and an active pixel array size (W x H), shading map entry + * (x,y) ϵ (0 ... N-1, 0 ... M-1) is the value of the shading map at + * pixel ( ((W-1)/(N-1)) * x, ((H-1)/(M-1)) * y) for the four color channels. + * The map is assumed to be bilinearly interpolated between the sample points.</p> + * <p>The channel order is [R, Geven, Godd, B], where Geven is the green + * channel for the even rows of a Bayer pattern, and Godd is the odd rows. + * The shading map is stored in a fully interleaved format, and its size + * is provided in the camera static metadata by {@link CameraCharacteristics#LENS_INFO_SHADING_MAP_SIZE android.lens.info.shadingMapSize}.</p> + * <p>The shading map should have on the order of 30-40 rows and columns, + * and must be smaller than 64x64.</p> + * <p>As an example, given a very small map defined as:</p> + * <pre><code>{@link CameraCharacteristics#LENS_INFO_SHADING_MAP_SIZE android.lens.info.shadingMapSize} = [ 4, 3 ] + * {@link CaptureResult#STATISTICS_LENS_SHADING_MAP android.statistics.lensShadingMap} = + * [ 1.3, 1.2, 1.15, 1.2, 1.2, 1.2, 1.15, 1.2, + * 1.1, 1.2, 1.2, 1.2, 1.3, 1.2, 1.3, 1.3, + * 1.2, 1.2, 1.25, 1.1, 1.1, 1.1, 1.1, 1.0, + * 1.0, 1.0, 1.0, 1.0, 1.2, 1.3, 1.25, 1.2, + * 1.3, 1.2, 1.2, 1.3, 1.2, 1.15, 1.1, 1.2, + * 1.2, 1.1, 1.0, 1.2, 1.3, 1.15, 1.2, 1.3 ] + * </code></pre> + * <p>The low-resolution scaling map images for each channel are + * (displayed using nearest-neighbor interpolation):</p> + * <p><img alt="Red lens shading map" src="../../../../images/camera2/metadata/android.statistics.lensShadingMap/red_shading.png" /> + * <img alt="Green (even rows) lens shading map" src="../../../../images/camera2/metadata/android.statistics.lensShadingMap/green_e_shading.png" /> + * <img alt="Green (odd rows) lens shading map" src="../../../../images/camera2/metadata/android.statistics.lensShadingMap/green_o_shading.png" /> + * <img alt="Blue lens shading map" src="../../../../images/camera2/metadata/android.statistics.lensShadingMap/blue_shading.png" /></p> + * <p>As a visualization only, inverting the full-color map to recover an + * image of a gray wall (using bicubic interpolation for visual quality) as captured by the sensor gives:</p> + * <p><img alt="Image of a uniform white wall (inverse shading map)" src="../../../../images/camera2/metadata/android.statistics.lensShadingMap/inv_shading.png" /></p> + * + * @see CaptureRequest#COLOR_CORRECTION_MODE + * @see CameraCharacteristics#LENS_INFO_SHADING_MAP_SIZE + * @see CaptureResult#STATISTICS_LENS_SHADING_MAP */ public static final Key<float[]> STATISTICS_LENS_SHADING_MAP = new Key<float[]>("android.statistics.lensShadingMap", float[].class); /** - * <p> - * The best-fit color channel gains calculated - * by the HAL's statistics units for the current output frame - * </p> - * <p> - * This may be different than the gains used for this frame, + * <p>The best-fit color channel gains calculated + * by the camera device's statistics units for the current output frame.</p> + * <p>This may be different than the gains used for this frame, * since statistics processing on data from a new frame * typically completes after the transform has already been - * applied to that frame. - * </p><p> - * The 4 channel gains are defined in Bayer domain, - * see android.colorCorrection.gains for details. - * </p><p> - * This value should always be calculated by the AWB block, - * regardless of the android.control.* current values. - * </p> + * applied to that frame.</p> + * <p>The 4 channel gains are defined in Bayer domain, + * see {@link CaptureRequest#COLOR_CORRECTION_GAINS android.colorCorrection.gains} for details.</p> + * <p>This value should always be calculated by the AWB block, + * regardless of the android.control.* current values.</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + * + * @see CaptureRequest#COLOR_CORRECTION_GAINS + * @hide */ public static final Key<float[]> STATISTICS_PREDICTED_COLOR_GAINS = new Key<float[]>("android.statistics.predictedColorGains", float[].class); /** - * <p> - * The best-fit color transform matrix estimate - * calculated by the HAL's statistics units for the current - * output frame - * </p> - * <p> - * The HAL must provide the estimate from its + * <p>The best-fit color transform matrix estimate + * calculated by the camera device's statistics units for the current + * output frame.</p> + * <p>The camera device will provide the estimate from its * statistics unit on the white balance transforms to use - * for the next frame. These are the values the HAL believes + * for the next frame. These are the values the camera device believes * are the best fit for the current output frame. This may * be different than the transform used for this frame, since * statistics processing on data from a new frame typically * completes after the transform has already been applied to - * that frame. - * </p><p> - * These estimates must be provided for all frames, even if - * capture settings and color transforms are set by the application. - * </p><p> - * This value should always be calculated by the AWB block, - * regardless of the android.control.* current values. - * </p> + * that frame.</p> + * <p>These estimates must be provided for all frames, even if + * capture settings and color transforms are set by the application.</p> + * <p>This value should always be calculated by the AWB block, + * regardless of the android.control.* current values.</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + * @hide */ public static final Key<Rational[]> STATISTICS_PREDICTED_COLOR_TRANSFORM = new Key<Rational[]>("android.statistics.predictedColorTransform", Rational[].class); /** - * <p> - * The HAL estimated scene illumination lighting - * frequency - * </p> - * <p> - * Report NONE if there doesn't appear to be flickering - * illumination - * </p> + * <p>The camera device estimated scene illumination lighting + * frequency.</p> + * <p>Many light sources, such as most fluorescent lights, flicker at a rate + * that depends on the local utility power standards. This flicker must be + * accounted for by auto-exposure routines to avoid artifacts in captured images. + * The camera device uses this entry to tell the application what the scene + * illuminant frequency is.</p> + * <p>When manual exposure control is enabled + * (<code>{@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} == OFF</code> or <code>{@link CaptureRequest#CONTROL_MODE android.control.mode} == OFF</code>), + * the {@link CaptureRequest#CONTROL_AE_ANTIBANDING_MODE android.control.aeAntibandingMode} doesn't do the antibanding, and the + * application can ensure it selects exposure times that do not cause banding + * issues by looking into this metadata field. See {@link CaptureRequest#CONTROL_AE_ANTIBANDING_MODE android.control.aeAntibandingMode} + * for more details.</p> + * <p>Report NONE if there doesn't appear to be flickering illumination.</p> + * + * @see CaptureRequest#CONTROL_AE_ANTIBANDING_MODE + * @see CaptureRequest#CONTROL_AE_MODE + * @see CaptureRequest#CONTROL_MODE * @see #STATISTICS_SCENE_FLICKER_NONE * @see #STATISTICS_SCENE_FLICKER_50HZ * @see #STATISTICS_SCENE_FLICKER_60HZ @@ -908,61 +1972,137 @@ public final class CaptureResult extends CameraMetadata { new Key<Integer>("android.statistics.sceneFlicker", int.class); /** - * <p> - * Table mapping blue input values to output - * values - * </p> - * <p> - * Tonemapping / contrast / gamma curve for the blue - * channel, to use when android.tonemap.mode is CONTRAST_CURVE. - * </p><p> - * See android.tonemap.curveRed for more details. - * </p> + * <p>Operating mode for hotpixel map generation.</p> + * <p>If set to ON, a hotpixel map is returned in {@link CaptureResult#STATISTICS_HOT_PIXEL_MAP android.statistics.hotPixelMap}. + * If set to OFF, no hotpixel map should be returned.</p> + * <p>This must be set to a valid mode from {@link CameraCharacteristics#STATISTICS_INFO_AVAILABLE_HOT_PIXEL_MAP_MODES android.statistics.info.availableHotPixelMapModes}.</p> + * + * @see CaptureResult#STATISTICS_HOT_PIXEL_MAP + * @see CameraCharacteristics#STATISTICS_INFO_AVAILABLE_HOT_PIXEL_MAP_MODES + */ + public static final Key<Boolean> STATISTICS_HOT_PIXEL_MAP_MODE = + new Key<Boolean>("android.statistics.hotPixelMapMode", boolean.class); + + /** + * <p>List of <code>(x, y)</code> coordinates of hot/defective pixels on the sensor.</p> + * <p>A coordinate <code>(x, y)</code> must lie between <code>(0, 0)</code>, and + * <code>(width - 1, height - 1)</code> (inclusive), which are the top-left and + * bottom-right of the pixel array, respectively. The width and + * height dimensions are given in {@link CameraCharacteristics#SENSOR_INFO_PIXEL_ARRAY_SIZE android.sensor.info.pixelArraySize}. + * This may include hot pixels that lie outside of the active array + * bounds given by {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE android.sensor.info.activeArraySize}.</p> + * + * @see CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE + * @see CameraCharacteristics#SENSOR_INFO_PIXEL_ARRAY_SIZE + */ + public static final Key<int[]> STATISTICS_HOT_PIXEL_MAP = + new Key<int[]>("android.statistics.hotPixelMap", int[].class); + + /** + * <p>Tonemapping / contrast / gamma curve for the blue + * channel, to use when {@link CaptureRequest#TONEMAP_MODE android.tonemap.mode} is + * CONTRAST_CURVE.</p> + * <p>See {@link CaptureRequest#TONEMAP_CURVE_RED android.tonemap.curveRed} for more details.</p> + * + * @see CaptureRequest#TONEMAP_CURVE_RED + * @see CaptureRequest#TONEMAP_MODE */ public static final Key<float[]> TONEMAP_CURVE_BLUE = new Key<float[]>("android.tonemap.curveBlue", float[].class); /** - * <p> - * Table mapping green input values to output - * values - * </p> - * <p> - * Tonemapping / contrast / gamma curve for the green - * channel, to use when android.tonemap.mode is CONTRAST_CURVE. - * </p><p> - * See android.tonemap.curveRed for more details. - * </p> + * <p>Tonemapping / contrast / gamma curve for the green + * channel, to use when {@link CaptureRequest#TONEMAP_MODE android.tonemap.mode} is + * CONTRAST_CURVE.</p> + * <p>See {@link CaptureRequest#TONEMAP_CURVE_RED android.tonemap.curveRed} for more details.</p> + * + * @see CaptureRequest#TONEMAP_CURVE_RED + * @see CaptureRequest#TONEMAP_MODE */ public static final Key<float[]> TONEMAP_CURVE_GREEN = new Key<float[]>("android.tonemap.curveGreen", float[].class); /** - * <p> - * Table mapping red input values to output - * values - * </p> - * <p> - * Tonemapping / contrast / gamma curve for the red - * channel, to use when android.tonemap.mode is CONTRAST_CURVE. - * </p><p> - * Since the input and output ranges may vary depending on - * the camera pipeline, the input and output pixel values - * are represented by normalized floating-point values - * between 0 and 1, with 0 == black and 1 == white. - * </p><p> - * The curve should be linearly interpolated between the - * defined points. The points will be listed in increasing - * order of P_IN. For example, if the array is: [0.0, 0.0, - * 0.3, 0.5, 1.0, 1.0], then the input->output mapping - * for a few sample points would be: 0 -> 0, 0.15 -> - * 0.25, 0.3 -> 0.5, 0.5 -> 0.64 - * </p> + * <p>Tonemapping / contrast / gamma curve for the red + * channel, to use when {@link CaptureRequest#TONEMAP_MODE android.tonemap.mode} is + * CONTRAST_CURVE.</p> + * <p>Each channel's curve is defined by an array of control points:</p> + * <pre><code>{@link CaptureRequest#TONEMAP_CURVE_RED android.tonemap.curveRed} = + * [ P0in, P0out, P1in, P1out, P2in, P2out, P3in, P3out, ..., PNin, PNout ] + * 2 <= N <= {@link CameraCharacteristics#TONEMAP_MAX_CURVE_POINTS android.tonemap.maxCurvePoints}</code></pre> + * <p>These are sorted in order of increasing <code>Pin</code>; it is always + * guaranteed that input values 0.0 and 1.0 are included in the list to + * define a complete mapping. For input values between control points, + * the camera device must linearly interpolate between the control + * points.</p> + * <p>Each curve can have an independent number of points, and the number + * of points can be less than max (that is, the request doesn't have to + * always provide a curve with number of points equivalent to + * {@link CameraCharacteristics#TONEMAP_MAX_CURVE_POINTS android.tonemap.maxCurvePoints}).</p> + * <p>A few examples, and their corresponding graphical mappings; these + * only specify the red channel and the precision is limited to 4 + * digits, for conciseness.</p> + * <p>Linear mapping:</p> + * <pre><code>{@link CaptureRequest#TONEMAP_CURVE_RED android.tonemap.curveRed} = [ 0, 0, 1.0, 1.0 ] + * </code></pre> + * <p><img alt="Linear mapping curve" src="../../../../images/camera2/metadata/android.tonemap.curveRed/linear_tonemap.png" /></p> + * <p>Invert mapping:</p> + * <pre><code>{@link CaptureRequest#TONEMAP_CURVE_RED android.tonemap.curveRed} = [ 0, 1.0, 1.0, 0 ] + * </code></pre> + * <p><img alt="Inverting mapping curve" src="../../../../images/camera2/metadata/android.tonemap.curveRed/inverse_tonemap.png" /></p> + * <p>Gamma 1/2.2 mapping, with 16 control points:</p> + * <pre><code>{@link CaptureRequest#TONEMAP_CURVE_RED android.tonemap.curveRed} = [ + * 0.0000, 0.0000, 0.0667, 0.2920, 0.1333, 0.4002, 0.2000, 0.4812, + * 0.2667, 0.5484, 0.3333, 0.6069, 0.4000, 0.6594, 0.4667, 0.7072, + * 0.5333, 0.7515, 0.6000, 0.7928, 0.6667, 0.8317, 0.7333, 0.8685, + * 0.8000, 0.9035, 0.8667, 0.9370, 0.9333, 0.9691, 1.0000, 1.0000 ] + * </code></pre> + * <p><img alt="Gamma = 1/2.2 tonemapping curve" src="../../../../images/camera2/metadata/android.tonemap.curveRed/gamma_tonemap.png" /></p> + * <p>Standard sRGB gamma mapping, per IEC 61966-2-1:1999, with 16 control points:</p> + * <pre><code>{@link CaptureRequest#TONEMAP_CURVE_RED android.tonemap.curveRed} = [ + * 0.0000, 0.0000, 0.0667, 0.2864, 0.1333, 0.4007, 0.2000, 0.4845, + * 0.2667, 0.5532, 0.3333, 0.6125, 0.4000, 0.6652, 0.4667, 0.7130, + * 0.5333, 0.7569, 0.6000, 0.7977, 0.6667, 0.8360, 0.7333, 0.8721, + * 0.8000, 0.9063, 0.8667, 0.9389, 0.9333, 0.9701, 1.0000, 1.0000 ] + * </code></pre> + * <p><img alt="sRGB tonemapping curve" src="../../../../images/camera2/metadata/android.tonemap.curveRed/srgb_tonemap.png" /></p> + * + * @see CaptureRequest#TONEMAP_CURVE_RED + * @see CameraCharacteristics#TONEMAP_MAX_CURVE_POINTS + * @see CaptureRequest#TONEMAP_MODE */ public static final Key<float[]> TONEMAP_CURVE_RED = new Key<float[]>("android.tonemap.curveRed", float[].class); /** + * <p>High-level global contrast/gamma/tonemapping control.</p> + * <p>When switching to an application-defined contrast curve by setting + * {@link CaptureRequest#TONEMAP_MODE android.tonemap.mode} to CONTRAST_CURVE, the curve is defined + * per-channel with a set of <code>(in, out)</code> points that specify the + * mapping from input high-bit-depth pixel value to the output + * low-bit-depth value. Since the actual pixel ranges of both input + * and output may change depending on the camera pipeline, the values + * are specified by normalized floating-point numbers.</p> + * <p>More-complex color mapping operations such as 3D color look-up + * tables, selective chroma enhancement, or other non-linear color + * transforms will be disabled when {@link CaptureRequest#TONEMAP_MODE android.tonemap.mode} is + * CONTRAST_CURVE.</p> + * <p>This must be set to a valid mode in + * {@link CameraCharacteristics#TONEMAP_AVAILABLE_TONE_MAP_MODES android.tonemap.availableToneMapModes}.</p> + * <p>When using either FAST or HIGH_QUALITY, the camera device will + * emit its own tonemap curve in {@link CaptureRequest#TONEMAP_CURVE_RED android.tonemap.curveRed}, + * {@link CaptureRequest#TONEMAP_CURVE_GREEN android.tonemap.curveGreen}, and {@link CaptureRequest#TONEMAP_CURVE_BLUE android.tonemap.curveBlue}. + * These values are always available, and as close as possible to the + * actually used nonlinear/nonglobal transforms.</p> + * <p>If a request is sent with TRANSFORM_MATRIX with the camera device's + * provided curve in FAST or HIGH_QUALITY, the image's tonemap will be + * roughly the same.</p> + * + * @see CameraCharacteristics#TONEMAP_AVAILABLE_TONE_MAP_MODES + * @see CaptureRequest#TONEMAP_CURVE_BLUE + * @see CaptureRequest#TONEMAP_CURVE_GREEN + * @see CaptureRequest#TONEMAP_CURVE_RED + * @see CaptureRequest#TONEMAP_MODE * @see #TONEMAP_MODE_CONTRAST_CURVE * @see #TONEMAP_MODE_FAST * @see #TONEMAP_MODE_HIGH_QUALITY @@ -971,53 +2111,95 @@ public final class CaptureResult extends CameraMetadata { new Key<Integer>("android.tonemap.mode", int.class); /** - * <p> - * This LED is nominally used to indicate to the user + * <p>This LED is nominally used to indicate to the user * that the camera is powered on and may be streaming images back to the * Application Processor. In certain rare circumstances, the OS may * disable this when video is processed locally and not transmitted to - * any untrusted applications. - * </p><p> - * In particular, the LED *must* always be on when the data could be - * transmitted off the device. The LED *should* always be on whenever - * data is stored locally on the device. - * </p><p> - * The LED *may* be off if a trusted application is using the data that - * doesn't violate the above rules. - * </p> - * + * any untrusted applications.</p> + * <p>In particular, the LED <em>must</em> always be on when the data could be + * transmitted off the device. The LED <em>should</em> always be on whenever + * data is stored locally on the device.</p> + * <p>The LED <em>may</em> be off if a trusted application is using the data that + * doesn't violate the above rules.</p> * @hide */ public static final Key<Boolean> LED_TRANSMIT = new Key<Boolean>("android.led.transmit", boolean.class); /** - * <p> - * Whether black-level compensation is locked - * to its current values, or is free to vary - * </p> - * <p> - * When set to ON, the values used for black-level - * compensation must not change until the lock is set to - * OFF - * </p><p> - * Since changes to certain capture parameters (such as - * exposure time) may require resetting of black level - * compensation, the HAL must report whether setting the - * black level lock was successful in the output result - * metadata. - * </p><p> - * The black level locking must happen at the sensor, and not at the ISP. - * If for some reason black level locking is no longer legal (for example, - * the analog gain has changed, which forces black levels to be - * recalculated), then the HAL is free to override this request (and it - * must report 'OFF' when this does happen) until the next time locking - * is legal again. - * </p> + * <p>Whether black-level compensation is locked + * to its current values, or is free to vary.</p> + * <p>Whether the black level offset was locked for this frame. Should be + * ON if {@link CaptureRequest#BLACK_LEVEL_LOCK android.blackLevel.lock} was ON in the capture request, unless + * a change in other capture settings forced the camera device to + * perform a black level reset.</p> + * + * @see CaptureRequest#BLACK_LEVEL_LOCK */ public static final Key<Boolean> BLACK_LEVEL_LOCK = new Key<Boolean>("android.blackLevel.lock", boolean.class); + /** + * <p>The frame number corresponding to the last request + * with which the output result (metadata + buffers) has been fully + * synchronized.</p> + * <p>When a request is submitted to the camera device, there is usually a + * delay of several frames before the controls get applied. A camera + * device may either choose to account for this delay by implementing a + * pipeline and carefully submit well-timed atomic control updates, or + * it may start streaming control changes that span over several frame + * boundaries.</p> + * <p>In the latter case, whenever a request's settings change relative to + * the previous submitted request, the full set of changes may take + * multiple frame durations to fully take effect. Some settings may + * take effect sooner (in less frame durations) than others.</p> + * <p>While a set of control changes are being propagated, this value + * will be CONVERGING.</p> + * <p>Once it is fully known that a set of control changes have been + * finished propagating, and the resulting updated control settings + * have been read back by the camera device, this value will be set + * to a non-negative frame number (corresponding to the request to + * which the results have synchronized to).</p> + * <p>Older camera device implementations may not have a way to detect + * when all camera controls have been applied, and will always set this + * value to UNKNOWN.</p> + * <p>FULL capability devices will always have this value set to the + * frame number of the request corresponding to this result.</p> + * <p><em>Further details</em>:</p> + * <ul> + * <li>Whenever a request differs from the last request, any future + * results not yet returned may have this value set to CONVERGING (this + * could include any in-progress captures not yet returned by the camera + * device, for more details see pipeline considerations below).</li> + * <li>Submitting a series of multiple requests that differ from the + * previous request (e.g. r1, r2, r3 s.t. r1 != r2 != r3) + * moves the new synchronization frame to the last non-repeating + * request (using the smallest frame number from the contiguous list of + * repeating requests).</li> + * <li>Submitting the same request repeatedly will not change this value + * to CONVERGING, if it was already a non-negative value.</li> + * <li>When this value changes to non-negative, that means that all of the + * metadata controls from the request have been applied, all of the + * metadata controls from the camera device have been read to the + * updated values (into the result), and all of the graphics buffers + * corresponding to this result are also synchronized to the request.</li> + * </ul> + * <p><em>Pipeline considerations</em>:</p> + * <p>Submitting a request with updated controls relative to the previously + * submitted requests may also invalidate the synchronization state + * of all the results corresponding to currently in-flight requests.</p> + * <p>In other words, results for this current request and up to + * {@link CameraCharacteristics#REQUEST_PIPELINE_MAX_DEPTH android.request.pipelineMaxDepth} prior requests may have their + * android.sync.frameNumber change to CONVERGING.</p> + * + * @see CameraCharacteristics#REQUEST_PIPELINE_MAX_DEPTH + * @see #SYNC_FRAME_NUMBER_CONVERGING + * @see #SYNC_FRAME_NUMBER_UNKNOWN + * @hide + */ + public static final Key<Integer> SYNC_FRAME_NUMBER = + new Key<Integer>("android.sync.frameNumber", int.class); + /*~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~ * End generated code *~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~O@*/ diff --git a/core/java/android/hardware/camera2/CaptureResultExtras.aidl b/core/java/android/hardware/camera2/CaptureResultExtras.aidl new file mode 100644 index 0000000..6587f02 --- /dev/null +++ b/core/java/android/hardware/camera2/CaptureResultExtras.aidl @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2014 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.camera2; + +/** @hide */ +parcelable CaptureResultExtras; diff --git a/core/java/android/hardware/camera2/CaptureResultExtras.java b/core/java/android/hardware/camera2/CaptureResultExtras.java new file mode 100644 index 0000000..e5c2c1c --- /dev/null +++ b/core/java/android/hardware/camera2/CaptureResultExtras.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2014 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.camera2; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * @hide + */ +public class CaptureResultExtras implements Parcelable { + private int requestId; + private int subsequenceId; + private int afTriggerId; + private int precaptureTriggerId; + private long frameNumber; + + public static final Parcelable.Creator<CaptureResultExtras> CREATOR = + new Parcelable.Creator<CaptureResultExtras>() { + @Override + public CaptureResultExtras createFromParcel(Parcel in) { + return new CaptureResultExtras(in); + } + + @Override + public CaptureResultExtras[] newArray(int size) { + return new CaptureResultExtras[size]; + } + }; + + private CaptureResultExtras(Parcel in) { + readFromParcel(in); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(requestId); + dest.writeInt(subsequenceId); + dest.writeInt(afTriggerId); + dest.writeInt(precaptureTriggerId); + dest.writeLong(frameNumber); + } + + public void readFromParcel(Parcel in) { + requestId = in.readInt(); + subsequenceId = in.readInt(); + afTriggerId = in.readInt(); + precaptureTriggerId = in.readInt(); + frameNumber = in.readLong(); + } + + public int getRequestId() { + return requestId; + } + + public int getSubsequenceId() { + return subsequenceId; + } + + public int getAfTriggerId() { + return afTriggerId; + } + + public int getPrecaptureTriggerId() { + return precaptureTriggerId; + } + + public long getFrameNumber() { + return frameNumber; + } + +} diff --git a/core/java/android/hardware/camera2/ICameraDeviceCallbacks.aidl b/core/java/android/hardware/camera2/ICameraDeviceCallbacks.aidl index 02a73d66..a14d38b 100644 --- a/core/java/android/hardware/camera2/ICameraDeviceCallbacks.aidl +++ b/core/java/android/hardware/camera2/ICameraDeviceCallbacks.aidl @@ -17,6 +17,7 @@ package android.hardware.camera2; import android.hardware.camera2.impl.CameraMetadataNative; +import android.hardware.camera2.CaptureResultExtras; /** @hide */ interface ICameraDeviceCallbacks @@ -25,8 +26,9 @@ interface ICameraDeviceCallbacks * Keep up-to-date with frameworks/av/include/camera/camera2/ICameraDeviceCallbacks.h */ - oneway void onCameraError(int errorCode); + oneway void onCameraError(int errorCode, in CaptureResultExtras resultExtras); oneway void onCameraIdle(); - oneway void onCaptureStarted(int requestId, long timestamp); - oneway void onResultReceived(int requestId, in CameraMetadataNative result); + oneway void onCaptureStarted(in CaptureResultExtras resultExtras, long timestamp); + oneway void onResultReceived(in CameraMetadataNative result, + in CaptureResultExtras resultExtras); } diff --git a/core/java/android/hardware/camera2/ICameraDeviceUser.aidl b/core/java/android/hardware/camera2/ICameraDeviceUser.aidl index 1936963..d77f3d1 100644 --- a/core/java/android/hardware/camera2/ICameraDeviceUser.aidl +++ b/core/java/android/hardware/camera2/ICameraDeviceUser.aidl @@ -20,6 +20,8 @@ import android.view.Surface; import android.hardware.camera2.impl.CameraMetadataNative; import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.LongParcelable; + /** @hide */ interface ICameraDeviceUser { @@ -31,9 +33,13 @@ interface ICameraDeviceUser // ints here are status_t // non-negative value is the requestId. negative value is status_t - int submitRequest(in CaptureRequest request, boolean streaming); + int submitRequest(in CaptureRequest request, boolean streaming, + out LongParcelable lastFrameNumber); + + int submitRequestList(in List<CaptureRequest> requestList, boolean streaming, + out LongParcelable lastFrameNumber); - int cancelRequest(int requestId); + int cancelRequest(int requestId, out LongParcelable lastFrameNumber); int deleteStream(int streamId); @@ -46,5 +52,5 @@ interface ICameraDeviceUser int waitUntilIdle(); - int flush(); + int flush(out LongParcelable lastFrameNumber); } diff --git a/core/java/android/hardware/camera2/LongParcelable.aidl b/core/java/android/hardware/camera2/LongParcelable.aidl new file mode 100644 index 0000000..7d7e51b --- /dev/null +++ b/core/java/android/hardware/camera2/LongParcelable.aidl @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2014 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.camera2; + +/** @hide */ +parcelable LongParcelable;
\ No newline at end of file diff --git a/core/java/android/hardware/camera2/LongParcelable.java b/core/java/android/hardware/camera2/LongParcelable.java new file mode 100644 index 0000000..97b0631 --- /dev/null +++ b/core/java/android/hardware/camera2/LongParcelable.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2014 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.camera2; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * @hide + */ +public class LongParcelable implements Parcelable { + private long number; + + public LongParcelable() { + this.number = 0; + } + + public LongParcelable(long number) { + this.number = number; + } + + public static final Parcelable.Creator<LongParcelable> CREATOR = + new Parcelable.Creator<LongParcelable>() { + @Override + public LongParcelable createFromParcel(Parcel in) { + return new LongParcelable(in); + } + + @Override + public LongParcelable[] newArray(int size) { + return new LongParcelable[size]; + } + }; + + private LongParcelable(Parcel in) { + readFromParcel(in); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(number); + } + + public void readFromParcel(Parcel in) { + number = in.readLong(); + } + + public long getNumber() { + return number; + } + + public void setNumber(long number) { + this.number = number; + } + +} diff --git a/core/java/android/hardware/camera2/impl/CameraDevice.java b/core/java/android/hardware/camera2/impl/CameraDevice.java index 40586f0..cd44b51 100644 --- a/core/java/android/hardware/camera2/impl/CameraDevice.java +++ b/core/java/android/hardware/camera2/impl/CameraDevice.java @@ -21,8 +21,10 @@ import static android.hardware.camera2.CameraAccessException.CAMERA_IN_USE; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.CaptureResult; +import android.hardware.camera2.CaptureResultExtras; import android.hardware.camera2.ICameraDeviceCallbacks; import android.hardware.camera2.ICameraDeviceUser; +import android.hardware.camera2.LongParcelable; import android.hardware.camera2.utils.CameraBinderDecorator; import android.hardware.camera2.utils.CameraRuntimeException; import android.os.Handler; @@ -33,10 +35,12 @@ import android.util.Log; import android.util.SparseArray; import android.view.Surface; +import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.TreeSet; /** * HAL2.1+ implementation of CameraDevice. Use CameraManager#open to instantiate @@ -69,10 +73,24 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { private final String mCameraId; + /** + * A list tracking request and its expected last frame. + * Updated when calling ICameraDeviceUser methods. + */ + private final List<SimpleEntry</*frameNumber*/Long, /*requestId*/Integer>> + mFrameNumberRequestPairs = new ArrayList<SimpleEntry<Long, Integer>>(); + + /** + * An object tracking received frame numbers. + * Updated when receiving callbacks from ICameraDeviceCallbacks. + */ + private final FrameNumberTracker mFrameNumberTracker = new FrameNumberTracker(); + // Runnables for all state transitions, except error, which needs the // error code argument private final Runnable mCallOnOpened = new Runnable() { + @Override public void run() { if (!CameraDevice.this.isClosed()) { mDeviceListener.onOpened(CameraDevice.this); @@ -81,6 +99,7 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { }; private final Runnable mCallOnUnconfigured = new Runnable() { + @Override public void run() { if (!CameraDevice.this.isClosed()) { mDeviceListener.onUnconfigured(CameraDevice.this); @@ -89,6 +108,7 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { }; private final Runnable mCallOnActive = new Runnable() { + @Override public void run() { if (!CameraDevice.this.isClosed()) { mDeviceListener.onActive(CameraDevice.this); @@ -97,6 +117,7 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { }; private final Runnable mCallOnBusy = new Runnable() { + @Override public void run() { if (!CameraDevice.this.isClosed()) { mDeviceListener.onBusy(CameraDevice.this); @@ -105,14 +126,14 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { }; private final Runnable mCallOnClosed = new Runnable() { + @Override public void run() { - if (!CameraDevice.this.isClosed()) { - mDeviceListener.onClosed(CameraDevice.this); - } + mDeviceListener.onClosed(CameraDevice.this); } }; private final Runnable mCallOnIdle = new Runnable() { + @Override public void run() { if (!CameraDevice.this.isClosed()) { mDeviceListener.onIdle(CameraDevice.this); @@ -121,6 +142,7 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { }; private final Runnable mCallOnDisconnected = new Runnable() { + @Override public void run() { if (!CameraDevice.this.isClosed()) { mDeviceListener.onDisconnected(CameraDevice.this); @@ -251,22 +273,26 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { @Override public int capture(CaptureRequest request, CaptureListener listener, Handler handler) throws CameraAccessException { - return submitCaptureRequest(request, listener, handler, /*streaming*/false); + if (DEBUG) { + Log.d(TAG, "calling capture"); + } + List<CaptureRequest> requestList = new ArrayList<CaptureRequest>(); + requestList.add(request); + return submitCaptureRequest(requestList, listener, handler, /*streaming*/false); } @Override public int captureBurst(List<CaptureRequest> requests, CaptureListener listener, Handler handler) throws CameraAccessException { + // TODO: remove this. Throw IAE if the request is null or empty. Need to update API doc. if (requests.isEmpty()) { Log.w(TAG, "Capture burst request list is empty, do nothing!"); return -1; } - // TODO - throw new UnsupportedOperationException("Burst capture implemented yet"); - + return submitCaptureRequest(requests, listener, handler, /*streaming*/false); } - private int submitCaptureRequest(CaptureRequest request, CaptureListener listener, + private int submitCaptureRequest(List<CaptureRequest> requestList, CaptureListener listener, Handler handler, boolean repeating) throws CameraAccessException { // Need a valid handler, or current thread needs to have a looper, if @@ -283,8 +309,13 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { stopRepeating(); } + LongParcelable lastFrameNumberRef = new LongParcelable(); try { - requestId = mRemoteDevice.submitRequest(request, repeating); + requestId = mRemoteDevice.submitRequestList(requestList, repeating, + /*out*/lastFrameNumberRef); + if (!repeating) { + Log.v(TAG, "last frame number " + lastFrameNumberRef.getNumber()); + } } catch (CameraRuntimeException e) { throw e.asChecked(); } catch (RemoteException e) { @@ -292,12 +323,29 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { return -1; } if (listener != null) { - mCaptureListenerMap.put(requestId, new CaptureListenerHolder(listener, request, - handler, repeating)); + mCaptureListenerMap.put(requestId, new CaptureListenerHolder(listener, + requestList, handler, repeating)); } + long lastFrameNumber = lastFrameNumberRef.getNumber(); + /** + * If it's the first repeating request, then returned lastFrameNumber can be + * negative. Otherwise, it should always be non-negative. + */ + if (((lastFrameNumber < 0) && (requestId > 0)) + || ((lastFrameNumber < 0) && (!repeating))) { + throw new AssertionError(String.format("returned bad frame number %d", + lastFrameNumber)); + } if (repeating) { + if (mRepeatingRequestId != REQUEST_ID_NONE) { + mFrameNumberRequestPairs.add( + new SimpleEntry<Long, Integer>(lastFrameNumber, mRepeatingRequestId)); + } mRepeatingRequestId = requestId; + } else { + mFrameNumberRequestPairs.add( + new SimpleEntry<Long, Integer>(lastFrameNumber, requestId)); } if (mIdle) { @@ -312,18 +360,20 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { @Override public int setRepeatingRequest(CaptureRequest request, CaptureListener listener, Handler handler) throws CameraAccessException { - return submitCaptureRequest(request, listener, handler, /*streaming*/true); + List<CaptureRequest> requestList = new ArrayList<CaptureRequest>(); + requestList.add(request); + return submitCaptureRequest(requestList, listener, handler, /*streaming*/true); } @Override public int setRepeatingBurst(List<CaptureRequest> requests, CaptureListener listener, Handler handler) throws CameraAccessException { + // TODO: remove this. Throw IAE if the request is null or empty. Need to update API doc. if (requests.isEmpty()) { Log.w(TAG, "Set Repeating burst request list is empty, do nothing!"); return -1; } - // TODO - throw new UnsupportedOperationException("Burst capture implemented yet"); + return submitCaptureRequest(requests, listener, handler, /*streaming*/true); } @Override @@ -337,10 +387,20 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { mRepeatingRequestId = REQUEST_ID_NONE; // Queue for deletion after in-flight requests finish - mRepeatingRequestIdDeletedList.add(requestId); + if (mCaptureListenerMap.get(requestId) != null) { + mRepeatingRequestIdDeletedList.add(requestId); + } try { - mRemoteDevice.cancelRequest(requestId); + LongParcelable lastFrameNumberRef = new LongParcelable(); + mRemoteDevice.cancelRequest(requestId, /*out*/lastFrameNumberRef); + long lastFrameNumber = lastFrameNumberRef.getNumber(); + if ((lastFrameNumber < 0) && (requestId > 0)) { + throw new AssertionError(String.format("returned bad frame number %d", + lastFrameNumber)); + } + mFrameNumberRequestPairs.add( + new SimpleEntry<Long, Integer>(lastFrameNumber, requestId)); } catch (CameraRuntimeException e) { throw e.asChecked(); } catch (RemoteException e) { @@ -351,8 +411,7 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { } } - @Override - public void waitUntilIdle() throws CameraAccessException { + private void waitUntilIdle() throws CameraAccessException { synchronized (mLock) { checkIfCameraClosed(); @@ -370,8 +429,6 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { } mRepeatingRequestId = REQUEST_ID_NONE; - mRepeatingRequestIdDeletedList.clear(); - mCaptureListenerMap.clear(); } } @@ -382,7 +439,17 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { mDeviceHandler.post(mCallOnBusy); try { - mRemoteDevice.flush(); + LongParcelable lastFrameNumberRef = new LongParcelable(); + mRemoteDevice.flush(/*out*/lastFrameNumberRef); + if (mRepeatingRequestId != REQUEST_ID_NONE) { + long lastFrameNumber = lastFrameNumberRef.getNumber(); + if (lastFrameNumber < 0) { + Log.e(TAG, String.format("returned bad frame number %d", lastFrameNumber)); + } + mFrameNumberRequestPairs.add( + new SimpleEntry<Long, Integer>(lastFrameNumber, mRepeatingRequestId)); + mRepeatingRequestId = REQUEST_ID_NONE; + } } catch (CameraRuntimeException e) { throw e.asChecked(); } catch (RemoteException e) { @@ -428,18 +495,18 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { private final boolean mRepeating; private final CaptureListener mListener; - private final CaptureRequest mRequest; + private final List<CaptureRequest> mRequestList; private final Handler mHandler; - CaptureListenerHolder(CaptureListener listener, CaptureRequest request, Handler handler, - boolean repeating) { + CaptureListenerHolder(CaptureListener listener, List<CaptureRequest> requestList, + Handler handler, boolean repeating) { if (listener == null || handler == null) { throw new UnsupportedOperationException( "Must have a valid handler and a valid listener"); } mRepeating = repeating; mHandler = handler; - mRequest = request; + mRequestList = new ArrayList<CaptureRequest>(requestList); mListener = listener; } @@ -451,8 +518,24 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { return mListener; } + public CaptureRequest getRequest(int subsequenceId) { + if (subsequenceId >= mRequestList.size()) { + throw new IllegalArgumentException( + String.format( + "Requested subsequenceId %d is larger than request list size %d.", + subsequenceId, mRequestList.size())); + } else { + if (subsequenceId < 0) { + throw new IllegalArgumentException(String.format( + "Requested subsequenceId %d is negative", subsequenceId)); + } else { + return mRequestList.get(subsequenceId); + } + } + } + public CaptureRequest getRequest() { - return mRequest; + return getRequest(0); } public Handler getHandler() { @@ -461,6 +544,105 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { } + /** + * This class tracks the last frame number for submitted requests. + */ + public class FrameNumberTracker { + + private long mCompletedFrameNumber = -1; + private final TreeSet<Long> mFutureErrorSet = new TreeSet<Long>(); + + private void update() { + Iterator<Long> iter = mFutureErrorSet.iterator(); + while (iter.hasNext()) { + long errorFrameNumber = iter.next(); + if (errorFrameNumber == mCompletedFrameNumber + 1) { + mCompletedFrameNumber++; + iter.remove(); + } else { + break; + } + } + } + + /** + * This function is called every time when a result or an error is received. + * @param frameNumber: the frame number corresponding to the result or error + * @param isError: true if it is an error, false if it is not an error + */ + public void updateTracker(long frameNumber, boolean isError) { + if (isError) { + mFutureErrorSet.add(frameNumber); + } else { + /** + * HAL cannot send an OnResultReceived for frame N unless it knows for + * sure that all frames prior to N have either errored out or completed. + * So if the current frame is not an error, then all previous frames + * should have arrived. The following line checks whether this holds. + */ + if (frameNumber != mCompletedFrameNumber + 1) { + throw new AssertionError(String.format( + "result frame number %d comes out of order", + frameNumber)); + } + mCompletedFrameNumber++; + } + update(); + } + + public long getCompletedFrameNumber() { + return mCompletedFrameNumber; + } + + } + + private void checkAndFireSequenceComplete() { + long completedFrameNumber = mFrameNumberTracker.getCompletedFrameNumber(); + Iterator<SimpleEntry<Long, Integer> > iter = mFrameNumberRequestPairs.iterator(); + while (iter.hasNext()) { + final SimpleEntry<Long, Integer> frameNumberRequestPair = iter.next(); + if (frameNumberRequestPair.getKey() <= completedFrameNumber) { + + // remove request from mCaptureListenerMap + final int requestId = frameNumberRequestPair.getValue(); + final CaptureListenerHolder holder; + synchronized (mLock) { + int index = CameraDevice.this.mCaptureListenerMap.indexOfKey(requestId); + holder = (index >= 0) ? CameraDevice.this.mCaptureListenerMap.valueAt(index) + : null; + if (holder != null) { + CameraDevice.this.mCaptureListenerMap.removeAt(index); + } + } + iter.remove(); + + // Call onCaptureSequenceCompleted + if (holder != null) { + Runnable resultDispatch = new Runnable() { + @Override + public void run() { + if (!CameraDevice.this.isClosed()){ + if (DEBUG) { + Log.d(TAG, String.format( + "fire sequence complete for request %d", + requestId)); + } + + holder.getListener().onCaptureSequenceCompleted( + CameraDevice.this, + requestId, + // TODO: this is problematic, crop long to int + frameNumberRequestPair.getKey().intValue()); + } + } + }; + holder.getHandler().post(resultDispatch); + } + + } + } + } + public class CameraDeviceCallbacks extends ICameraDeviceCallbacks.Stub { // @@ -495,7 +677,7 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { } @Override - public void onCameraError(final int errorCode) { + public void onCameraError(final int errorCode, CaptureResultExtras resultExtras) { Runnable r = null; if (isClosed()) return; @@ -510,6 +692,7 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { case ERROR_CAMERA_DEVICE: case ERROR_CAMERA_SERVICE: r = new Runnable() { + @Override public void run() { if (!CameraDevice.this.isClosed()) { mDeviceListener.onError(CameraDevice.this, errorCode); @@ -520,6 +703,11 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { } CameraDevice.this.mDeviceHandler.post(r); } + + // Fire onCaptureSequenceCompleted + mFrameNumberTracker.updateTracker(resultExtras.getFrameNumber(), /*error*/true); + checkAndFireSequenceComplete(); + } @Override @@ -538,7 +726,8 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { } @Override - public void onCaptureStarted(int requestId, final long timestamp) { + public void onCaptureStarted(final CaptureResultExtras resultExtras, final long timestamp) { + int requestId = resultExtras.getRequestId(); if (DEBUG) { Log.d(TAG, "Capture started for id " + requestId); } @@ -558,11 +747,12 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { // Dispatch capture start notice holder.getHandler().post( new Runnable() { + @Override public void run() { if (!CameraDevice.this.isClosed()) { holder.getListener().onCaptureStarted( CameraDevice.this, - holder.getRequest(), + holder.getRequest(resultExtras.getSubsequenceId()), timestamp); } } @@ -570,48 +760,18 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { } @Override - public void onResultReceived(int requestId, CameraMetadataNative result) - throws RemoteException { + public void onResultReceived(CameraMetadataNative result, + CaptureResultExtras resultExtras) throws RemoteException { + int requestId = resultExtras.getRequestId(); if (DEBUG) { Log.d(TAG, "Received result for id " + requestId); } - final CaptureListenerHolder holder; + final CaptureListenerHolder holder = + CameraDevice.this.mCaptureListenerMap.get(requestId); Boolean quirkPartial = result.get(CaptureResult.QUIRKS_PARTIAL_RESULT); boolean quirkIsPartialResult = (quirkPartial != null && quirkPartial); - synchronized (mLock) { - // TODO: move this whole map into this class to make it more testable, - // exposing the methods necessary like subscribeToRequest, unsubscribe.. - // TODO: make class static class - - holder = CameraDevice.this.mCaptureListenerMap.get(requestId); - - // Clean up listener once we no longer expect to see it. - if (holder != null && !holder.isRepeating() && !quirkIsPartialResult) { - CameraDevice.this.mCaptureListenerMap.remove(requestId); - } - - // TODO: add 'capture sequence completed' callback to the - // service, and clean up repeating requests there instead. - - // If we received a result for a repeating request and have - // prior repeating requests queued for deletion, remove those - // requests from mCaptureListenerMap. - if (holder != null && holder.isRepeating() && !quirkIsPartialResult - && mRepeatingRequestIdDeletedList.size() > 0) { - Iterator<Integer> iter = mRepeatingRequestIdDeletedList.iterator(); - while (iter.hasNext()) { - int deletedRequestId = iter.next(); - if (deletedRequestId < requestId) { - CameraDevice.this.mCaptureListenerMap.remove(deletedRequestId); - iter.remove(); - } - } - } - - } - // Check if we have a listener for this if (holder == null) { return; @@ -619,7 +779,7 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { if (isClosed()) return; - final CaptureRequest request = holder.getRequest(); + final CaptureRequest request = holder.getRequest(resultExtras.getSubsequenceId()); final CaptureResult resultAsCapture = new CaptureResult(result, request, requestId); Runnable resultDispatch = null; @@ -654,6 +814,12 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { } holder.getHandler().post(resultDispatch); + + // Fire onCaptureSequenceCompleted + if (!quirkIsPartialResult) { + mFrameNumberTracker.updateTracker(resultExtras.getFrameNumber(), /*error*/false); + checkAndFireSequenceComplete(); + } } } diff --git a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java index 072c5bb..c5e5753 100644 --- a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java +++ b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java @@ -105,6 +105,18 @@ public class CameraMetadataNative extends CameraMetadata implements Parcelable { } /** + * Set the global client-side vendor tag descriptor to allow use of vendor + * tags in camera applications. + * + * @return int A native status_t value corresponding to one of the + * {@link CameraBinderDecorator} integer constants. + * @see CameraBinderDecorator#throwOnError + * + * @hide + */ + public static native int nativeSetupGlobalVendorTagDescriptor(); + + /** * Set a camera metadata field to a value. The field definitions can be * found in {@link CameraCharacteristics}, {@link CaptureResult}, and * {@link CaptureRequest}. @@ -448,7 +460,11 @@ public class CameraMetadataNative extends CameraMetadata implements Parcelable { } else if (key.equals(CaptureResult.STATISTICS_FACES)) { return (T) getFaces(); } else if (key.equals(CaptureResult.STATISTICS_FACE_RECTANGLES)) { - return (T) fixFaceRectangles(); + return (T) getFaceRectangles(); + } else if (key.equals(CameraCharacteristics.SCALER_AVAILABLE_STREAM_CONFIGURATIONS)) { + return (T) getAvailableStreamConfigurations(); + } else if (key.equals(CameraCharacteristics.SCALER_AVAILABLE_MIN_FRAME_DURATIONS)) { + return (T) getAvailableMinFrameDurations(); } // For other keys, get() falls back to getBase() @@ -457,15 +473,62 @@ public class CameraMetadataNative extends CameraMetadata implements Parcelable { private int[] getAvailableFormats() { int[] availableFormats = getBase(CameraCharacteristics.SCALER_AVAILABLE_FORMATS); - for (int i = 0; i < availableFormats.length; i++) { - // JPEG has different value between native and managed side, need override. - if (availableFormats[i] == NATIVE_JPEG_FORMAT) { - availableFormats[i] = ImageFormat.JPEG; + if (availableFormats != null) { + for (int i = 0; i < availableFormats.length; i++) { + // JPEG has different value between native and managed side, need override. + if (availableFormats[i] == NATIVE_JPEG_FORMAT) { + availableFormats[i] = ImageFormat.JPEG; + } } } + return availableFormats; } + private int[] getAvailableStreamConfigurations() { + final int NUM_ELEMENTS_IN_CONFIG = 4; + int[] availableConfigs = + getBase(CameraCharacteristics.SCALER_AVAILABLE_STREAM_CONFIGURATIONS); + if (availableConfigs != null) { + if (availableConfigs.length % NUM_ELEMENTS_IN_CONFIG != 0) { + Log.w(TAG, "availableStreamConfigurations is malformed, length must be multiple" + + " of " + NUM_ELEMENTS_IN_CONFIG); + return availableConfigs; + } + + for (int i = 0; i < availableConfigs.length; i += NUM_ELEMENTS_IN_CONFIG) { + // JPEG has different value between native and managed side, need override. + if (availableConfigs[i] == NATIVE_JPEG_FORMAT) { + availableConfigs[i] = ImageFormat.JPEG; + } + } + } + + return availableConfigs; + } + + private long[] getAvailableMinFrameDurations() { + final int NUM_ELEMENTS_IN_DURATION = 4; + long[] availableMinDurations = + getBase(CameraCharacteristics.SCALER_AVAILABLE_MIN_FRAME_DURATIONS); + if (availableMinDurations != null) { + if (availableMinDurations.length % NUM_ELEMENTS_IN_DURATION != 0) { + Log.w(TAG, "availableStreamConfigurations is malformed, length must be multiple" + + " of " + NUM_ELEMENTS_IN_DURATION); + return availableMinDurations; + } + + for (int i = 0; i < availableMinDurations.length; i += NUM_ELEMENTS_IN_DURATION) { + // JPEG has different value between native and managed side, need override. + if (availableMinDurations[i] == NATIVE_JPEG_FORMAT) { + availableMinDurations[i] = ImageFormat.JPEG; + } + } + } + + return availableMinDurations; + } + private Face[] getFaces() { final int FACE_LANDMARK_SIZE = 6; @@ -550,7 +613,7 @@ public class CameraMetadataNative extends CameraMetadata implements Parcelable { // (left, top, width, height) at the native level, so the normal Rect // conversion that does (l, t, w, h) -> (l, t, r, b) is unnecessary. Undo // that conversion here for just the faces. - private Rect[] fixFaceRectangles() { + private Rect[] getFaceRectangles() { Rect[] faceRectangles = getBase(CaptureResult.STATISTICS_FACE_RECTANGLES); if (faceRectangles == null) return null; @@ -590,12 +653,58 @@ public class CameraMetadataNative extends CameraMetadata implements Parcelable { private <T> boolean setOverride(Key<T> key, T value) { if (key.equals(CameraCharacteristics.SCALER_AVAILABLE_FORMATS)) { return setAvailableFormats((int[]) value); + } else if (key.equals(CaptureResult.STATISTICS_FACE_RECTANGLES)) { + return setFaceRectangles((Rect[]) value); + } else if (key.equals(CameraCharacteristics.SCALER_AVAILABLE_STREAM_CONFIGURATIONS)) { + return setAvailableStreamConfigurations((int[])value); + } else if (key.equals(CameraCharacteristics.SCALER_AVAILABLE_MIN_FRAME_DURATIONS)) { + return setAvailableMinFrameDurations((long[])value); } // For other keys, set() falls back to setBase(). return false; } + private boolean setAvailableStreamConfigurations(int[] value) { + final int NUM_ELEMENTS_IN_CONFIG = 4; + int[] availableConfigs = value; + if (value == null) { + // Let setBase() to handle the null value case. + return false; + } + + int[] newValues = new int[availableConfigs.length]; + for (int i = 0; i < availableConfigs.length; i++) { + newValues[i] = availableConfigs[i]; + if (i % NUM_ELEMENTS_IN_CONFIG == 0 && availableConfigs[i] == ImageFormat.JPEG) { + newValues[i] = NATIVE_JPEG_FORMAT; + } + } + + setBase(CameraCharacteristics.SCALER_AVAILABLE_STREAM_CONFIGURATIONS, newValues); + return true; + } + + private boolean setAvailableMinFrameDurations(long[] value) { + final int NUM_ELEMENTS_IN_DURATION = 4; + long[] availableDurations = value; + if (value == null) { + // Let setBase() to handle the null value case. + return false; + } + + long[] newValues = new long[availableDurations.length]; + for (int i = 0; i < availableDurations.length; i++) { + newValues[i] = availableDurations[i]; + if (i % NUM_ELEMENTS_IN_DURATION == 0 && availableDurations[i] == ImageFormat.JPEG) { + newValues[i] = NATIVE_JPEG_FORMAT; + } + } + + setBase(CameraCharacteristics.SCALER_AVAILABLE_MIN_FRAME_DURATIONS, newValues); + return true; + } + private boolean setAvailableFormats(int[] value) { int[] availableFormat = value; if (value == null) { @@ -615,6 +724,36 @@ public class CameraMetadataNative extends CameraMetadata implements Parcelable { return true; } + /** + * Convert Face Rectangles from managed side to native side as they have different definitions. + * <p> + * Managed side face rectangles are defined as: left, top, width, height. + * Native side face rectangles are defined as: left, top, right, bottom. + * The input face rectangle need to be converted to native side definition when set is called. + * </p> + * + * @param faceRects Input face rectangles. + * @return true if face rectangles can be set successfully. Otherwise, Let the caller + * (setBase) to handle it appropriately. + */ + private boolean setFaceRectangles(Rect[] faceRects) { + if (faceRects == null) { + return false; + } + + Rect[] newFaceRects = new Rect[faceRects.length]; + for (int i = 0; i < newFaceRects.length; i++) { + newFaceRects[i] = new Rect( + faceRects[i].left, + faceRects[i].top, + faceRects[i].right + faceRects[i].left, + faceRects[i].bottom + faceRects[i].top); + } + + setBase(CaptureResult.STATISTICS_FACE_RECTANGLES, newFaceRects); + return true; + } + private long mMetadataPtr; // native CameraMetadata* private native long nativeAllocate(); diff --git a/core/java/android/hardware/camera2/package.html b/core/java/android/hardware/camera2/package.html index c619984..9f6c2a9 100644 --- a/core/java/android/hardware/camera2/package.html +++ b/core/java/android/hardware/camera2/package.html @@ -80,7 +80,5 @@ output streams included in the request. These are produced asynchronously relative to the output CaptureResult, sometimes substantially later.</p> -{@hide} - </BODY> </HTML> diff --git a/core/java/android/hardware/camera2/utils/CameraBinderDecorator.java b/core/java/android/hardware/camera2/utils/CameraBinderDecorator.java index e535e00..328ccbe 100644 --- a/core/java/android/hardware/camera2/utils/CameraBinderDecorator.java +++ b/core/java/android/hardware/camera2/utils/CameraBinderDecorator.java @@ -64,47 +64,7 @@ public class CameraBinderDecorator { // int return type => status_t => convert to exception if (m.getReturnType() == Integer.TYPE) { int returnValue = (Integer) result; - - switch (returnValue) { - case NO_ERROR: - return; - case PERMISSION_DENIED: - throw new SecurityException("Lacking privileges to access camera service"); - case ALREADY_EXISTS: - // This should be handled at the call site. Typically this isn't bad, - // just means we tried to do an operation that already completed. - return; - case BAD_VALUE: - throw new IllegalArgumentException("Bad argument passed to camera service"); - case DEAD_OBJECT: - UncheckedThrow.throwAnyException(new CameraRuntimeException( - CAMERA_DISCONNECTED)); - case EACCES: - UncheckedThrow.throwAnyException(new CameraRuntimeException( - CAMERA_DISABLED)); - case EBUSY: - UncheckedThrow.throwAnyException(new CameraRuntimeException( - CAMERA_IN_USE)); - case EUSERS: - UncheckedThrow.throwAnyException(new CameraRuntimeException( - MAX_CAMERAS_IN_USE)); - case ENODEV: - UncheckedThrow.throwAnyException(new CameraRuntimeException( - CAMERA_DISCONNECTED)); - case EOPNOTSUPP: - UncheckedThrow.throwAnyException(new CameraRuntimeException( - CAMERA_DEPRECATED_HAL)); - } - - /** - * Trap the rest of the negative return values. If we have known - * error codes i.e. ALREADY_EXISTS that aren't really runtime - * errors, then add them to the top switch statement - */ - if (returnValue < 0) { - throw new UnsupportedOperationException(String.format("Unknown error %d", - returnValue)); - } + throwOnError(returnValue); } } @@ -131,6 +91,54 @@ public class CameraBinderDecorator { } /** + * Throw error codes returned by the camera service as exceptions. + * + * @param errorFlag error to throw as an exception. + */ + public static void throwOnError(int errorFlag) { + switch (errorFlag) { + case NO_ERROR: + return; + case PERMISSION_DENIED: + throw new SecurityException("Lacking privileges to access camera service"); + case ALREADY_EXISTS: + // This should be handled at the call site. Typically this isn't bad, + // just means we tried to do an operation that already completed. + return; + case BAD_VALUE: + throw new IllegalArgumentException("Bad argument passed to camera service"); + case DEAD_OBJECT: + UncheckedThrow.throwAnyException(new CameraRuntimeException( + CAMERA_DISCONNECTED)); + case EACCES: + UncheckedThrow.throwAnyException(new CameraRuntimeException( + CAMERA_DISABLED)); + case EBUSY: + UncheckedThrow.throwAnyException(new CameraRuntimeException( + CAMERA_IN_USE)); + case EUSERS: + UncheckedThrow.throwAnyException(new CameraRuntimeException( + MAX_CAMERAS_IN_USE)); + case ENODEV: + UncheckedThrow.throwAnyException(new CameraRuntimeException( + CAMERA_DISCONNECTED)); + case EOPNOTSUPP: + UncheckedThrow.throwAnyException(new CameraRuntimeException( + CAMERA_DEPRECATED_HAL)); + } + + /** + * Trap the rest of the negative return values. If we have known + * error codes i.e. ALREADY_EXISTS that aren't really runtime + * errors, then add them to the top switch statement + */ + if (errorFlag < 0) { + throw new UnsupportedOperationException(String.format("Unknown error %d", + errorFlag)); + } + } + + /** * <p> * Wraps the type T with a proxy that will check 'status_t' return codes * from the native side of the camera service, and throw Java exceptions diff --git a/core/java/android/hardware/display/WifiDisplayStatus.java b/core/java/android/hardware/display/WifiDisplayStatus.java index 5216727..b645662 100644 --- a/core/java/android/hardware/display/WifiDisplayStatus.java +++ b/core/java/android/hardware/display/WifiDisplayStatus.java @@ -20,8 +20,6 @@ import android.os.Parcel; import android.os.Parcelable; import java.util.Arrays; -import java.util.ArrayList; -import java.util.List; /** * Describes the current global state of Wifi display connectivity, including the diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl index f1e7e98..465d142 100644 --- a/core/java/android/hardware/input/IInputManager.aidl +++ b/core/java/android/hardware/input/IInputManager.aidl @@ -19,6 +19,7 @@ package android.hardware.input; import android.hardware.input.InputDeviceIdentifier; import android.hardware.input.KeyboardLayout; import android.hardware.input.IInputDevicesChangedListener; +import android.hardware.input.TouchCalibration; import android.os.IBinder; import android.view.InputDevice; import android.view.InputEvent; @@ -39,6 +40,11 @@ interface IInputManager { // applications, the caller must have the INJECT_EVENTS permission. boolean injectInputEvent(in InputEvent ev, int mode); + // Calibrate input device position + TouchCalibration getTouchCalibrationForInputDevice(String inputDeviceDescriptor, int rotation); + void setTouchCalibrationForInputDevice(String inputDeviceDescriptor, int rotation, + in TouchCalibration calibration); + // Keyboard layouts configuration. KeyboardLayout[] getKeyboardLayouts(); KeyboardLayout getKeyboardLayout(String keyboardLayoutDescriptor); diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index a2aeafb..b5efa8e 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -500,6 +500,45 @@ public final class InputManager { } /** + * Gets the TouchCalibration applied to the specified input device's coordinates. + * + * @param inputDeviceDescriptor The input device descriptor. + * @return The TouchCalibration currently assigned for use with the given + * input device. If none is set, an identity TouchCalibration is returned. + * + * @hide + */ + public TouchCalibration getTouchCalibration(String inputDeviceDescriptor, int surfaceRotation) { + try { + return mIm.getTouchCalibrationForInputDevice(inputDeviceDescriptor, surfaceRotation); + } catch (RemoteException ex) { + Log.w(TAG, "Could not get calibration matrix for input device.", ex); + return TouchCalibration.IDENTITY; + } + } + + /** + * Sets the TouchCalibration to apply to the specified input device's coordinates. + * <p> + * This method may have the side-effect of causing the input device in question + * to be reconfigured. Requires {@link android.Manifest.permissions.SET_INPUT_CALIBRATION}. + * </p> + * + * @param inputDeviceDescriptor The input device descriptor. + * @param calibration The calibration to be applied + * + * @hide + */ + public void setTouchCalibration(String inputDeviceDescriptor, int surfaceRotation, + TouchCalibration calibration) { + try { + mIm.setTouchCalibrationForInputDevice(inputDeviceDescriptor, surfaceRotation, calibration); + } catch (RemoteException ex) { + Log.w(TAG, "Could not set calibration matrix for input device.", ex); + } + } + + /** * Gets the mouse pointer speed. * <p> * Only returns the permanent mouse pointer speed. Ignores any temporary pointer @@ -814,13 +853,21 @@ public final class InputManager { return true; } + /** + * @hide + */ @Override - public void vibrate(long milliseconds) { + public void vibrate(int uid, String opPkg, long milliseconds, + int streamHint) { vibrate(new long[] { 0, milliseconds}, -1); } + /** + * @hide + */ @Override - public void vibrate(long[] pattern, int repeat) { + public void vibrate(int uid, String opPkg, long[] pattern, int repeat, + int streamHint) { if (repeat >= pattern.length) { throw new ArrayIndexOutOfBoundsException(); } @@ -831,22 +878,6 @@ public final class InputManager { } } - /** - * @hide - */ - @Override - public void vibrate(int owningUid, String owningPackage, long milliseconds) { - vibrate(milliseconds); - } - - /** - * @hide - */ - @Override - public void vibrate(int owningUid, String owningPackage, long[] pattern, int repeat) { - vibrate(pattern, repeat); - } - @Override public void cancel() { try { diff --git a/core/java/android/hardware/input/TouchCalibration.aidl b/core/java/android/hardware/input/TouchCalibration.aidl new file mode 100644 index 0000000..2c28774 --- /dev/null +++ b/core/java/android/hardware/input/TouchCalibration.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2014 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.input; + +parcelable TouchCalibration; diff --git a/core/java/android/hardware/input/TouchCalibration.java b/core/java/android/hardware/input/TouchCalibration.java new file mode 100644 index 0000000..025fad0 --- /dev/null +++ b/core/java/android/hardware/input/TouchCalibration.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2014 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.input; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Encapsulates calibration data for input devices. + * + * @hide + */ +public class TouchCalibration implements Parcelable { + + public static final TouchCalibration IDENTITY = new TouchCalibration(); + + public static final Parcelable.Creator<TouchCalibration> CREATOR + = new Parcelable.Creator<TouchCalibration>() { + public TouchCalibration createFromParcel(Parcel in) { + return new TouchCalibration(in); + } + + public TouchCalibration[] newArray(int size) { + return new TouchCalibration[size]; + } + }; + + private final float mXScale, mXYMix, mXOffset; + private final float mYXMix, mYScale, mYOffset; + + /** + * Create a new TouchCalibration initialized to the identity transformation. + */ + public TouchCalibration() { + this(1,0,0,0,1,0); + } + + /** + * Create a new TouchCalibration from affine transformation paramters. + * @param xScale Influence of input x-axis value on output x-axis value. + * @param xyMix Influence of input y-axis value on output x-axis value. + * @param xOffset Constant offset to be applied to output x-axis value. + * @param yXMix Influence of input x-axis value on output y-axis value. + * @param yScale Influence of input y-axis value on output y-axis value. + * @param yOffset Constant offset to be applied to output y-axis value. + */ + public TouchCalibration(float xScale, float xyMix, float xOffset, + float yxMix, float yScale, float yOffset) { + mXScale = xScale; + mXYMix = xyMix; + mXOffset = xOffset; + mYXMix = yxMix; + mYScale = yScale; + mYOffset = yOffset; + } + + public TouchCalibration(Parcel in) { + mXScale = in.readFloat(); + mXYMix = in.readFloat(); + mXOffset = in.readFloat(); + mYXMix = in.readFloat(); + mYScale = in.readFloat(); + mYOffset = in.readFloat(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeFloat(mXScale); + dest.writeFloat(mXYMix); + dest.writeFloat(mXOffset); + dest.writeFloat(mYXMix); + dest.writeFloat(mYScale); + dest.writeFloat(mYOffset); + } + + @Override + public int describeContents() { + return 0; + } + + public float[] getAffineTransform() { + return new float[] { mXScale, mXYMix, mXOffset, mYXMix, mYScale, mYOffset }; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } else if (obj instanceof TouchCalibration) { + TouchCalibration cal = (TouchCalibration)obj; + + return (cal.mXScale == mXScale) && + (cal.mXYMix == mXYMix) && + (cal.mXOffset == mXOffset) && + (cal.mYXMix == mYXMix) && + (cal.mYScale == mYScale) && + (cal.mYOffset == mYOffset); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Float.floatToIntBits(mXScale) ^ + Float.floatToIntBits(mXYMix) ^ + Float.floatToIntBits(mXOffset) ^ + Float.floatToIntBits(mYXMix) ^ + Float.floatToIntBits(mYScale) ^ + Float.floatToIntBits(mYOffset); + } +} diff --git a/core/java/android/hardware/location/GeofenceHardwareRequest.java b/core/java/android/hardware/location/GeofenceHardwareRequest.java index 6e7b592..796d7f8 100644 --- a/core/java/android/hardware/location/GeofenceHardwareRequest.java +++ b/core/java/android/hardware/location/GeofenceHardwareRequest.java @@ -16,8 +16,6 @@ package android.hardware.location; -import android.location.Location; - /** * This class represents the characteristics of the geofence. * diff --git a/core/java/android/hardware/usb/UsbAccessory.java b/core/java/android/hardware/usb/UsbAccessory.java index 5719452..2f9178c 100644 --- a/core/java/android/hardware/usb/UsbAccessory.java +++ b/core/java/android/hardware/usb/UsbAccessory.java @@ -16,10 +16,8 @@ package android.hardware.usb; -import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; -import android.util.Log; /** * A class representing a USB accessory, which is an external hardware component diff --git a/core/java/android/hardware/usb/UsbConfiguration.java b/core/java/android/hardware/usb/UsbConfiguration.java new file mode 100644 index 0000000..92d6f75 --- /dev/null +++ b/core/java/android/hardware/usb/UsbConfiguration.java @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2014 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.usb; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * A class representing a configuration on a {@link UsbDevice}. + * A USB configuration can have one or more interfaces, each one providing a different + * piece of functionality, separate from the other interfaces. + * An interface will have one or more {@link UsbEndpoint}s, which are the + * channels by which the host transfers data with the device. + * + * <div class="special reference"> + * <h3>Developer Guides</h3> + * <p>For more information about communicating with USB hardware, read the + * <a href="{@docRoot}guide/topics/usb/index.html">USB</a> developer guide.</p> + * </div> + */ +public class UsbConfiguration implements Parcelable { + + private final int mId; + private final String mName; + private final int mAttributes; + private final int mMaxPower; + private Parcelable[] mInterfaces; + + /** + * Mask for "self-powered" bit in the configuration's attributes. + * @see #getAttributes + */ + public static final int ATTR_SELF_POWERED_MASK = 1 << 6; + + /** + * Mask for "remote wakeup" bit in the configuration's attributes. + * @see #getAttributes + */ + public static final int ATTR_REMOTE_WAKEUP_MASK = 1 << 5; + + /** + * UsbConfiguration should only be instantiated by UsbService implementation + * @hide + */ + public UsbConfiguration(int id, String name, int attributes, int maxPower) { + mId = id; + mName = name; + mAttributes = attributes; + mMaxPower = maxPower; + } + + /** + * Returns the configuration's ID field. + * This is an integer that uniquely identifies the configuration on the device. + * + * @return the configuration's ID + */ + public int getId() { + return mId; + } + + /** + * Returns the configuration's name. + * + * @return the configuration's name + */ + public String getName() { + return mName; + } + + /** + * Returns the configuration's attributes field. + * This field contains a bit field with the following flags: + * + * Bit 7: always set to 1 + * Bit 6: self-powered + * Bit 5: remote wakeup enabled + * Bit 0-4: reserved + * @see #ATTR_SELF_POWERED_MASK + * @see #ATTR_REMOTE_WAKEUP_MASK + * @return the configuration's attributes + */ + public int getAttributes() { + return mAttributes; + } + + /** + * Returns the configuration's max power consumption, in milliamps. + * + * @return the configuration's max power + */ + public int getMaxPower() { + return mMaxPower * 2; + } + + /** + * Returns the number of {@link UsbInterface}s this configuration contains. + * + * @return the number of endpoints + */ + public int getInterfaceCount() { + return mInterfaces.length; + } + + /** + * Returns the {@link UsbInterface} at the given index. + * + * @return the interface + */ + public UsbInterface getInterface(int index) { + return (UsbInterface)mInterfaces[index]; + } + + /** + * Only used by UsbService implementation + * @hide + */ + public void setInterfaces(Parcelable[] interfaces) { + mInterfaces = interfaces; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("UsbConfiguration[mId=" + mId + + ",mName=" + mName + ",mAttributes=" + mAttributes + + ",mMaxPower=" + mMaxPower + ",mInterfaces=["); + for (int i = 0; i < mInterfaces.length; i++) { + builder.append("\n"); + builder.append(mInterfaces[i].toString()); + } + builder.append("]"); + return builder.toString(); + } + + public static final Parcelable.Creator<UsbConfiguration> CREATOR = + new Parcelable.Creator<UsbConfiguration>() { + public UsbConfiguration createFromParcel(Parcel in) { + int id = in.readInt(); + String name = in.readString(); + int attributes = in.readInt(); + int maxPower = in.readInt(); + Parcelable[] interfaces = in.readParcelableArray(UsbInterface.class.getClassLoader()); + UsbConfiguration configuration = new UsbConfiguration(id, name, attributes, maxPower); + configuration.setInterfaces(interfaces); + return configuration; + } + + public UsbConfiguration[] newArray(int size) { + return new UsbConfiguration[size]; + } + }; + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeInt(mId); + parcel.writeString(mName); + parcel.writeInt(mAttributes); + parcel.writeInt(mMaxPower); + parcel.writeParcelableArray(mInterfaces, 0); + } +} diff --git a/core/java/android/hardware/usb/UsbDevice.java b/core/java/android/hardware/usb/UsbDevice.java index d1e63f6..d90e06e 100644 --- a/core/java/android/hardware/usb/UsbDevice.java +++ b/core/java/android/hardware/usb/UsbDevice.java @@ -16,12 +16,8 @@ package android.hardware.usb; -import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; -import android.util.Log; - -import java.io.FileDescriptor; /** * This class represents a USB device attached to the android device with the android device @@ -54,7 +50,10 @@ public class UsbDevice implements Parcelable { private final int mClass; private final int mSubclass; private final int mProtocol; - private final Parcelable[] mInterfaces; + private Parcelable[] mConfigurations; + + // list of all interfaces on the device + private UsbInterface[] mInterfaces; /** * UsbDevice should only be instantiated by UsbService implementation @@ -62,8 +61,7 @@ public class UsbDevice implements Parcelable { */ public UsbDevice(String name, int vendorId, int productId, int Class, int subClass, int protocol, - String manufacturerName, String productName, String serialNumber, - Parcelable[] interfaces) { + String manufacturerName, String productName, String serialNumber) { mName = name; mVendorId = vendorId; mProductId = productId; @@ -73,7 +71,6 @@ public class UsbDevice implements Parcelable { mManufacturerName = manufacturerName; mProductName = productName; mSerialNumber = serialNumber; - mInterfaces = interfaces; } /** @@ -173,21 +170,74 @@ public class UsbDevice implements Parcelable { } /** + * Returns the number of {@link UsbConfiguration}s this device contains. + * + * @return the number of configurations + */ + public int getConfigurationCount() { + return mConfigurations.length; + } + + /** + * Returns the {@link UsbConfiguration} at the given index. + * + * @return the configuration + */ + public UsbConfiguration getConfiguration(int index) { + return (UsbConfiguration)mConfigurations[index]; + } + + private UsbInterface[] getInterfaceList() { + if (mInterfaces == null) { + int configurationCount = mConfigurations.length; + int interfaceCount = 0; + for (int i = 0; i < configurationCount; i++) { + UsbConfiguration configuration = (UsbConfiguration)mConfigurations[i]; + interfaceCount += configuration.getInterfaceCount(); + } + + mInterfaces = new UsbInterface[interfaceCount]; + int offset = 0; + for (int i = 0; i < configurationCount; i++) { + UsbConfiguration configuration = (UsbConfiguration)mConfigurations[i]; + interfaceCount = configuration.getInterfaceCount(); + for (int j = 0; j < interfaceCount; j++) { + mInterfaces[offset++] = configuration.getInterface(j); + } + } + } + + return mInterfaces; + } + + /** * Returns the number of {@link UsbInterface}s this device contains. + * For devices with multiple configurations, you will probably want to use + * {@link UsbConfiguration#getInterfaceCount} instead. * * @return the number of interfaces */ public int getInterfaceCount() { - return mInterfaces.length; + return getInterfaceList().length; } /** * Returns the {@link UsbInterface} at the given index. + * For devices with multiple configurations, you will probably want to use + * {@link UsbConfiguration#getInterface} instead. * * @return the interface */ public UsbInterface getInterface(int index) { - return (UsbInterface)mInterfaces[index]; + return getInterfaceList()[index]; + } + + /** + * Only used by UsbService implementation + * @hide + */ + public void setConfigurations(Parcelable[] configuration) { + mConfigurations = configuration; } @Override @@ -208,11 +258,17 @@ public class UsbDevice implements Parcelable { @Override public String toString() { - return "UsbDevice[mName=" + mName + ",mVendorId=" + mVendorId + - ",mProductId=" + mProductId + ",mClass=" + mClass + - ",mSubclass=" + mSubclass + ",mProtocol=" + mProtocol + + StringBuilder builder = new StringBuilder("UsbDevice[mName=" + mName + + ",mVendorId=" + mVendorId + ",mProductId=" + mProductId + + ",mClass=" + mClass + ",mSubclass=" + mSubclass + ",mProtocol=" + mProtocol + ",mManufacturerName=" + mManufacturerName + ",mProductName=" + mProductName + - ",mSerialNumber=" + mSerialNumber + ",mInterfaces=" + mInterfaces + "]"; + ",mSerialNumber=" + mSerialNumber + ",mConfigurations=["); + for (int i = 0; i < mConfigurations.length; i++) { + builder.append("\n"); + builder.append(mConfigurations[i].toString()); + } + builder.append("]"); + return builder.toString(); } public static final Parcelable.Creator<UsbDevice> CREATOR = @@ -227,9 +283,11 @@ public class UsbDevice implements Parcelable { String manufacturerName = in.readString(); String productName = in.readString(); String serialNumber = in.readString(); - Parcelable[] interfaces = in.readParcelableArray(UsbInterface.class.getClassLoader()); - return new UsbDevice(name, vendorId, productId, clasz, subClass, protocol, - manufacturerName, productName, serialNumber, interfaces); + Parcelable[] configurations = in.readParcelableArray(UsbInterface.class.getClassLoader()); + UsbDevice device = new UsbDevice(name, vendorId, productId, clasz, subClass, protocol, + manufacturerName, productName, serialNumber); + device.setConfigurations(configurations); + return device; } public UsbDevice[] newArray(int size) { @@ -251,7 +309,7 @@ public class UsbDevice implements Parcelable { parcel.writeString(mManufacturerName); parcel.writeString(mProductName); parcel.writeString(mSerialNumber); - parcel.writeParcelableArray(mInterfaces, 0); + parcel.writeParcelableArray(mConfigurations, 0); } public static int getDeviceId(String name) { diff --git a/core/java/android/hardware/usb/UsbDeviceConnection.java b/core/java/android/hardware/usb/UsbDeviceConnection.java index 389475f..6283951 100644 --- a/core/java/android/hardware/usb/UsbDeviceConnection.java +++ b/core/java/android/hardware/usb/UsbDeviceConnection.java @@ -101,6 +101,25 @@ public class UsbDeviceConnection { } /** + * Sets the current {@link android.hardware.usb.UsbInterface}. + * Used to select between two interfaces with the same ID but different alternate setting. + * + * @return true if the interface was successfully released + */ + public boolean setInterface(UsbInterface intf) { + return native_set_interface(intf.getId(), intf.getAlternateSetting()); + } + + /** + * Sets the device's current {@link android.hardware.usb.UsbConfiguration}. + * + * @return true if the configuration was successfully set + */ + public boolean setConfiguration(UsbConfiguration configuration) { + return native_set_configuration(configuration.getId()); + } + + /** * Performs a control transaction on endpoint zero for this device. * The direction of the transfer is determined by the request type. * If requestType & {@link UsbConstants#USB_ENDPOINT_DIR_MASK} is @@ -236,6 +255,8 @@ public class UsbDeviceConnection { private native byte[] native_get_desc(); private native boolean native_claim_interface(int interfaceID, boolean force); private native boolean native_release_interface(int interfaceID); + private native boolean native_set_interface(int interfaceID, int alternateSetting); + private native boolean native_set_configuration(int configurationID); private native int native_control_request(int requestType, int request, int value, int index, byte[] buffer, int offset, int length, int timeout); private native int native_bulk_request(int endpoint, byte[] buffer, diff --git a/core/java/android/hardware/usb/UsbEndpoint.java b/core/java/android/hardware/usb/UsbEndpoint.java index 753a447..708d651 100644 --- a/core/java/android/hardware/usb/UsbEndpoint.java +++ b/core/java/android/hardware/usb/UsbEndpoint.java @@ -16,7 +16,6 @@ package android.hardware.usb; -import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; diff --git a/core/java/android/hardware/usb/UsbInterface.java b/core/java/android/hardware/usb/UsbInterface.java index d6c54a8..de01a88 100644 --- a/core/java/android/hardware/usb/UsbInterface.java +++ b/core/java/android/hardware/usb/UsbInterface.java @@ -16,7 +16,6 @@ package android.hardware.usb; -import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; @@ -36,27 +35,31 @@ import android.os.Parcelable; public class UsbInterface implements Parcelable { private final int mId; + private final int mAlternateSetting; + private final String mName; private final int mClass; private final int mSubclass; private final int mProtocol; - private final Parcelable[] mEndpoints; + private Parcelable[] mEndpoints; /** * UsbInterface should only be instantiated by UsbService implementation * @hide */ - public UsbInterface(int id, int Class, int subClass, int protocol, - Parcelable[] endpoints) { + public UsbInterface(int id, int alternateSetting, String name, + int Class, int subClass, int protocol) { mId = id; + mAlternateSetting = alternateSetting; + mName = name; mClass = Class; mSubclass = subClass; mProtocol = protocol; - mEndpoints = endpoints; } /** - * Returns the interface's ID field. - * This is an integer that uniquely identifies the interface on the device. + * Returns the interface's bInterfaceNumber field. + * This is an integer that along with the alternate setting uniquely identifies + * the interface on the device. * * @return the interface's ID */ @@ -65,6 +68,28 @@ public class UsbInterface implements Parcelable { } /** + * Returns the interface's bAlternateSetting field. + * This is an integer that along with the ID uniquely identifies + * the interface on the device. + * {@link UsbDeviceConnection#setInterface} can be used to switch between + * two interfaces with the same ID but different alternate setting. + * + * @return the interface's alternate setting + */ + public int getAlternateSetting() { + return mAlternateSetting; + } + + /** + * Returns the interface's name. + * + * @return the interface's name + */ + public String getName() { + return mName; + } + + /** * Returns the interface's class field. * Some useful constants for USB classes can be found in {@link UsbConstants} * @@ -110,22 +135,42 @@ public class UsbInterface implements Parcelable { return (UsbEndpoint)mEndpoints[index]; } + /** + * Only used by UsbService implementation + * @hide + */ + public void setEndpoints(Parcelable[] endpoints) { + mEndpoints = endpoints; + } + @Override public String toString() { - return "UsbInterface[mId=" + mId + ",mClass=" + mClass + + StringBuilder builder = new StringBuilder("UsbInterface[mId=" + mId + + ",mAlternateSetting=" + mAlternateSetting + + ",mName=" + mName + ",mClass=" + mClass + ",mSubclass=" + mSubclass + ",mProtocol=" + mProtocol + - ",mEndpoints=" + mEndpoints + "]"; + ",mEndpoints=["); + for (int i = 0; i < mEndpoints.length; i++) { + builder.append("\n"); + builder.append(mEndpoints[i].toString()); + } + builder.append("]"); + return builder.toString(); } public static final Parcelable.Creator<UsbInterface> CREATOR = new Parcelable.Creator<UsbInterface>() { public UsbInterface createFromParcel(Parcel in) { int id = in.readInt(); + int alternateSetting = in.readInt(); + String name = in.readString(); int Class = in.readInt(); int subClass = in.readInt(); int protocol = in.readInt(); Parcelable[] endpoints = in.readParcelableArray(UsbEndpoint.class.getClassLoader()); - return new UsbInterface(id, Class, subClass, protocol, endpoints); + UsbInterface intf = new UsbInterface(id, alternateSetting, name, Class, subClass, protocol); + intf.setEndpoints(endpoints); + return intf; } public UsbInterface[] newArray(int size) { @@ -139,6 +184,8 @@ public class UsbInterface implements Parcelable { public void writeToParcel(Parcel parcel, int flags) { parcel.writeInt(mId); + parcel.writeInt(mAlternateSetting); + parcel.writeString(mName); parcel.writeInt(mClass); parcel.writeInt(mSubclass); parcel.writeInt(mProtocol); diff --git a/core/java/android/inputmethodservice/ExtractButton.java b/core/java/android/inputmethodservice/ExtractButton.java index b6b7595..fe63c1e 100644 --- a/core/java/android/inputmethodservice/ExtractButton.java +++ b/core/java/android/inputmethodservice/ExtractButton.java @@ -32,8 +32,12 @@ class ExtractButton extends Button { super(context, attrs, com.android.internal.R.attr.buttonStyle); } - public ExtractButton(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public ExtractButton(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ExtractButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); } /** diff --git a/core/java/android/inputmethodservice/ExtractEditText.java b/core/java/android/inputmethodservice/ExtractEditText.java index 23ae21b..48b604c 100644 --- a/core/java/android/inputmethodservice/ExtractEditText.java +++ b/core/java/android/inputmethodservice/ExtractEditText.java @@ -38,8 +38,12 @@ public class ExtractEditText extends EditText { super(context, attrs, com.android.internal.R.attr.editTextStyle); } - public ExtractEditText(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public ExtractEditText(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ExtractEditText(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); } void setIME(InputMethodService ime) { diff --git a/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java b/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java index bbea8ff..8437228 100644 --- a/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java +++ b/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java @@ -25,7 +25,6 @@ import android.graphics.Rect; import android.os.Bundle; import android.os.Looper; import android.os.Message; -import android.os.RemoteException; import android.util.Log; import android.util.SparseArray; import android.view.InputChannel; diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index 1b7d9ea..c51d1a7 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -249,6 +249,16 @@ public class InputMethodService extends AbstractInputMethodService { */ public static final int IME_VISIBLE = 0x2; + /** + * The IME does not require cursor/anchor position. + */ + public static final int CURSOR_ANCHOR_MONITOR_MODE_NONE = 0x0; + + /** + * The IME expects that {@link #onUpdateCursor(Rect)} is called back. + */ + public static final int CURSOR_ANCHOR_MONITOR_MODE_CURSOR_RECT = 0x1; + InputMethodManager mImm; int mTheme = 0; @@ -647,6 +657,7 @@ public class InputMethodService extends AbstractInputMethodService { getApplicationInfo().targetSdkVersion, android.R.style.Theme_InputMethod, android.R.style.Theme_Holo_InputMethod, + android.R.style.Theme_DeviceDefault_InputMethod, android.R.style.Theme_DeviceDefault_InputMethod); super.setTheme(mTheme); super.onCreate(); @@ -1701,6 +1712,13 @@ public class InputMethodService extends AbstractInputMethodService { } /** + * Update the cursor/anthor monitor mode. + */ + public void setCursorAnchorMonitorMode(int monitorMode) { + mImm.setCursorAnchorMonitorMode(mToken, monitorMode); + } + + /** * Close this input method's soft input area, removing it from the display. * The input method will continue running, but the user can no longer use * it to generate input by touching the screen. @@ -2322,6 +2340,21 @@ public class InputMethodService extends AbstractInputMethodService { } /** + * @return The recommended height of the input method window. + * An IME author can get the last input method's height as the recommended height + * by calling this in + * {@link android.inputmethodservice.InputMethodService#onStartInputView(EditorInfo, boolean)}. + * If you don't need to use a predefined fixed height, you can avoid the window-resizing of IME + * switching by using this value as a visible inset height. It's efficient for the smooth + * transition between different IMEs. However, note that this may return 0 (or possibly + * unexpectedly low height). You should thus avoid relying on the return value of this method + * all the time. Please make sure to use a reasonable height for the IME. + */ + public int getInputMethodWindowRecommendedHeight() { + return mImm.getInputMethodWindowVisibleHeight(); + } + + /** * Performs a dump of the InputMethodService's internal state. Override * to add your own information to the dump. */ diff --git a/core/java/android/inputmethodservice/KeyboardView.java b/core/java/android/inputmethodservice/KeyboardView.java index 4916244..af75a0a 100644 --- a/core/java/android/inputmethodservice/KeyboardView.java +++ b/core/java/android/inputmethodservice/KeyboardView.java @@ -279,12 +279,15 @@ public class KeyboardView extends View implements View.OnClickListener { this(context, attrs, com.android.internal.R.attr.keyboardViewStyle); } - public KeyboardView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public KeyboardView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public KeyboardView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = - context.obtainStyledAttributes( - attrs, android.R.styleable.KeyboardView, defStyle, 0); + TypedArray a = context.obtainStyledAttributes( + attrs, android.R.styleable.KeyboardView, defStyleAttr, defStyleRes); LayoutInflater inflate = (LayoutInflater) context diff --git a/core/java/android/net/BaseNetworkStateTracker.java b/core/java/android/net/BaseNetworkStateTracker.java index 476fefe..804f8ee 100644 --- a/core/java/android/net/BaseNetworkStateTracker.java +++ b/core/java/android/net/BaseNetworkStateTracker.java @@ -20,10 +20,10 @@ import android.content.Context; import android.os.Handler; import android.os.Messenger; -import com.android.internal.util.Preconditions; - import java.util.concurrent.atomic.AtomicBoolean; +import com.android.internal.util.Preconditions; + /** * Interface to control and observe state of a specific network, hiding * network-specific details from {@link ConnectivityManager}. Surfaces events @@ -108,11 +108,6 @@ public abstract class BaseNetworkStateTracker implements NetworkStateTracker { } @Override - public void captivePortalCheckComplete() { - // not implemented - } - - @Override public void captivePortalCheckCompleted(boolean isCaptivePortal) { // not implemented } diff --git a/core/java/android/net/CaptivePortalTracker.java b/core/java/android/net/CaptivePortalTracker.java index d678f1e..89c17c7 100644 --- a/core/java/android/net/CaptivePortalTracker.java +++ b/core/java/android/net/CaptivePortalTracker.java @@ -48,7 +48,6 @@ import java.io.IOException; import java.net.HttpURLConnection; import java.net.InetAddress; import java.net.Inet4Address; -import java.net.SocketTimeoutException; import java.net.URL; import java.net.UnknownHostException; import java.util.List; @@ -84,13 +83,12 @@ public class CaptivePortalTracker extends StateMachine { private String mServer; private String mUrl; private boolean mIsCaptivePortalCheckEnabled = false; - private IConnectivityManager mConnService; - private TelephonyManager mTelephonyManager; - private WifiManager mWifiManager; - private Context mContext; + private final IConnectivityManager mConnService; + private final TelephonyManager mTelephonyManager; + private final WifiManager mWifiManager; + private final Context mContext; private NetworkInfo mNetworkInfo; - private static final int CMD_DETECT_PORTAL = 0; private static final int CMD_CONNECTIVITY_CHANGE = 1; private static final int CMD_DELAYED_CAPTIVE_CHECK = 2; @@ -98,14 +96,15 @@ public class CaptivePortalTracker extends StateMachine { private static final int DELAYED_CHECK_INTERVAL_MS = 10000; private int mDelayedCheckToken = 0; - private State mDefaultState = new DefaultState(); - private State mNoActiveNetworkState = new NoActiveNetworkState(); - private State mActiveNetworkState = new ActiveNetworkState(); - private State mDelayedCaptiveCheckState = new DelayedCaptiveCheckState(); + private final State mDefaultState = new DefaultState(); + private final State mNoActiveNetworkState = new NoActiveNetworkState(); + private final State mActiveNetworkState = new ActiveNetworkState(); + private final State mDelayedCaptiveCheckState = new DelayedCaptiveCheckState(); private static final String SETUP_WIZARD_PACKAGE = "com.google.android.setupwizard"; private boolean mDeviceProvisioned = false; - private ProvisioningObserver mProvisioningObserver; + @SuppressWarnings("unused") + private final ProvisioningObserver mProvisioningObserver; private CaptivePortalTracker(Context context, IConnectivityManager cs) { super(TAG); @@ -174,29 +173,11 @@ public class CaptivePortalTracker extends StateMachine { return captivePortal; } - public void detectCaptivePortal(NetworkInfo info) { - sendMessage(obtainMessage(CMD_DETECT_PORTAL, info)); - } - private class DefaultState extends State { @Override public boolean processMessage(Message message) { - if (DBG) log(getName() + message.toString()); - switch (message.what) { - case CMD_DETECT_PORTAL: - NetworkInfo info = (NetworkInfo) message.obj; - // Checking on a secondary connection is not supported - // yet - notifyPortalCheckComplete(info); - break; - case CMD_CONNECTIVITY_CHANGE: - case CMD_DELAYED_CAPTIVE_CHECK: - break; - default: - loge("Ignoring " + message); - break; - } + loge("Ignoring " + message); return HANDLED; } } @@ -316,19 +297,6 @@ public class CaptivePortalTracker extends StateMachine { } } - private void notifyPortalCheckComplete(NetworkInfo info) { - if (info == null) { - loge("notifyPortalCheckComplete on null"); - return; - } - try { - if (DBG) log("notifyPortalCheckComplete: ni=" + info); - mConnService.captivePortalCheckComplete(info); - } catch(RemoteException e) { - e.printStackTrace(); - } - } - private void notifyPortalCheckCompleted(NetworkInfo info, boolean isCaptivePortal) { if (info == null) { loge("notifyPortalCheckComplete on null"); @@ -453,6 +421,13 @@ public class CaptivePortalTracker extends StateMachine { case ConnectivityManager.TYPE_WIFI: WifiInfo currentWifiInfo = mWifiManager.getConnectionInfo(); if (currentWifiInfo != null) { + // NOTE: getSSID()'s behavior changed in API 17; before that, SSIDs were not + // surrounded by double quotation marks (thus violating the Javadoc), but this + // was changed to match the Javadoc in API 17. Since clients may have started + // sanitizing the output of this method since API 17 was released, we should + // not change it here as it would become impossible to tell whether the SSID is + // simply being surrounded by quotes due to the API, or whether those quotes + // are actually part of the SSID. latencyBroadcast.putExtra(EXTRA_SSID, currentWifiInfo.getSSID()); latencyBroadcast.putExtra(EXTRA_BSSID, currentWifiInfo.getBSSID()); } else { @@ -464,7 +439,6 @@ public class CaptivePortalTracker extends StateMachine { latencyBroadcast.putExtra(EXTRA_NETWORK_TYPE, mTelephonyManager.getNetworkType()); List<CellInfo> info = mTelephonyManager.getAllCellInfo(); if (info == null) return; - StringBuffer uniqueCellId = new StringBuffer(); int numRegisteredCellInfo = 0; for (CellInfo cellInfo : info) { if (cellInfo.isRegistered()) { diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java index 4eecfa9..3da00b1 100644 --- a/core/java/android/net/ConnectivityManager.java +++ b/core/java/android/net/ConnectivityManager.java @@ -23,10 +23,14 @@ import android.annotation.SdkConstant.SdkConstantType; import android.content.Context; import android.os.Binder; import android.os.Build.VERSION_CODES; +import android.os.IBinder; +import android.os.INetworkActivityListener; +import android.os.INetworkManagementService; import android.os.Messenger; import android.os.RemoteException; -import android.os.ResultReceiver; +import android.os.ServiceManager; import android.provider.Settings; +import android.util.ArrayMap; import java.net.InetAddress; @@ -77,7 +81,7 @@ public class ConnectivityManager { /** * Identical to {@link #CONNECTIVITY_ACTION} broadcast, but sent without any - * applicable {@link Settings.Secure#CONNECTIVITY_CHANGE_DELAY}. + * applicable {@link Settings.Global#CONNECTIVITY_CHANGE_DELAY}. * * @hide */ @@ -171,6 +175,11 @@ public class ConnectivityManager { * {@hide} */ public static final String EXTRA_IS_ACTIVE = "isActive"; + /** + * The lookup key for a long that contains the timestamp (nanos) of the radio state change. + * {@hide} + */ + public static final String EXTRA_REALTIME_NS = "tsNanos"; /** * Broadcast Action: The setting for background data usage has changed @@ -403,6 +412,8 @@ public class ConnectivityManager { private final String mPackageName; + private INetworkManagementService mNMService; + /** * Tests if a given integer represents a valid network type. * @param networkType the type to be tested @@ -803,6 +814,8 @@ public class ConnectivityManager { * Ensure that a network route exists to deliver traffic to the specified * host via the specified network interface. An attempt to add a route that * already exists is ignored, but treated as successful. + * <p>This method requires the caller to hold the permission + * {@link android.Manifest.permission#CHANGE_NETWORK_STATE}. * @param networkType the type of the network over which traffic to the specified * host is to be routed * @param hostAddress the IP address of the host to which the route is desired @@ -906,6 +919,92 @@ public class ConnectivityManager { } /** + * Callback for use with {@link ConnectivityManager#registerNetworkActiveListener} to + * find out when the current network has gone in to a high power state. + */ + public interface OnNetworkActiveListener { + /** + * Called on the main thread of the process to report that the current data network + * has become active, and it is now a good time to perform any pending network + * operations. Note that this listener only tells you when the network becomes + * active; if at any other time you want to know whether it is active (and thus okay + * to initiate network traffic), you can retrieve its instantaneous state with + * {@link ConnectivityManager#isNetworkActive}. + */ + public void onNetworkActive(); + } + + private INetworkManagementService getNetworkManagementService() { + synchronized (this) { + if (mNMService != null) { + return mNMService; + } + IBinder b = ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE); + mNMService = INetworkManagementService.Stub.asInterface(b); + return mNMService; + } + } + + private final ArrayMap<OnNetworkActiveListener, INetworkActivityListener> + mNetworkActivityListeners + = new ArrayMap<OnNetworkActiveListener, INetworkActivityListener>(); + + /** + * Start listening to reports when the data network is active, meaning it is + * a good time to perform network traffic. Use {@link #isNetworkActive()} + * to determine the current state of the network after registering the listener. + * + * @param l The listener to be told when the network is active. + */ + public void registerNetworkActiveListener(final OnNetworkActiveListener l) { + INetworkActivityListener rl = new INetworkActivityListener.Stub() { + @Override + public void onNetworkActive() throws RemoteException { + l.onNetworkActive(); + } + }; + + try { + getNetworkManagementService().registerNetworkActivityListener(rl); + mNetworkActivityListeners.put(l, rl); + } catch (RemoteException e) { + } + } + + /** + * Remove network active listener previously registered with + * {@link #registerNetworkActiveListener}. + * + * @param l Previously registered listener. + */ + public void unregisterNetworkActiveListener(OnNetworkActiveListener l) { + INetworkActivityListener rl = mNetworkActivityListeners.get(l); + if (rl == null) { + throw new IllegalArgumentException("Listener not registered: " + l); + } + try { + getNetworkManagementService().unregisterNetworkActivityListener(rl); + } catch (RemoteException e) { + } + } + + /** + * Return whether the data network is currently active. An active network means that + * it is currently in a high power state for performing data transmission. On some + * types of networks, it may be expensive to move and stay in such a state, so it is + * more power efficient to batch network traffic together when the radio is already in + * this state. This method tells you whether right now is currently a good time to + * initiate network traffic, as the network is already active. + */ + public boolean isNetworkActive() { + try { + return getNetworkManagementService().isNetworkActive(); + } catch (RemoteException e) { + } + return false; + } + + /** * {@hide} */ public ConnectivityManager(IConnectivityManager service, String packageName) { @@ -1020,7 +1119,7 @@ public class ConnectivityManager { /** * Check if the device allows for tethering. It may be disabled via - * {@code ro.tether.denied} system property, {@link Settings#TETHER_SUPPORTED} or + * {@code ro.tether.denied} system property, Settings.TETHER_SUPPORTED or * due to device configuration. * * @return a boolean - {@code true} indicating Tethering is supported. @@ -1208,7 +1307,7 @@ public class ConnectivityManager { * doing something unusual like general internal filtering this may be useful. On * a private network where the proxy is not accessible, you may break HTTP using this. * - * @param proxyProperties The a {@link ProxyProperites} object defining the new global + * @param p The a {@link ProxyProperties} object defining the new global * HTTP proxy. A {@code null} value will clear the global HTTP proxy. * * <p>This method requires the call to hold the permission @@ -1341,24 +1440,6 @@ public class ConnectivityManager { /** * Signal that the captive portal check on the indicated network - * is complete and we can turn the network on for general use. - * - * @param info the {@link NetworkInfo} object for the networkType - * in question. - * - * <p>This method requires the call to hold the permission - * {@link android.Manifest.permission#CONNECTIVITY_INTERNAL}. - * {@hide} - */ - public void captivePortalCheckComplete(NetworkInfo info) { - try { - mService.captivePortalCheckComplete(info); - } catch (RemoteException e) { - } - } - - /** - * Signal that the captive portal check on the indicated network * is complete and whether its a captive portal or not. * * @param info the {@link NetworkInfo} object for the networkType @@ -1379,7 +1460,7 @@ public class ConnectivityManager { /** * Supply the backend messenger for a network tracker * - * @param type NetworkType to set + * @param networkType NetworkType to set * @param messenger {@link Messenger} * {@hide} */ diff --git a/core/java/android/net/DhcpInfo.java b/core/java/android/net/DhcpInfo.java index 3bede5d..788d7d9 100644 --- a/core/java/android/net/DhcpInfo.java +++ b/core/java/android/net/DhcpInfo.java @@ -18,7 +18,6 @@ package android.net; import android.os.Parcelable; import android.os.Parcel; -import java.net.InetAddress; /** * A simple object for retrieving the results of a DHCP request. diff --git a/core/java/android/net/DhcpResults.java b/core/java/android/net/DhcpResults.java index a3f70da..22b26b1 100644 --- a/core/java/android/net/DhcpResults.java +++ b/core/java/android/net/DhcpResults.java @@ -23,9 +23,6 @@ import android.util.Log; import java.net.InetAddress; import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; /** * A simple object for retrieving the results of a DHCP request. diff --git a/core/java/android/net/DummyDataStateTracker.java b/core/java/android/net/DummyDataStateTracker.java index 51a1191..a5d059e 100644 --- a/core/java/android/net/DummyDataStateTracker.java +++ b/core/java/android/net/DummyDataStateTracker.java @@ -117,11 +117,6 @@ public class DummyDataStateTracker extends BaseNetworkStateTracker { } @Override - public void captivePortalCheckComplete() { - // not implemented - } - - @Override public void captivePortalCheckCompleted(boolean isCaptivePortal) { // not implemented } diff --git a/core/java/android/net/EthernetDataTracker.java b/core/java/android/net/EthernetDataTracker.java index cc8c771..10b5d0b 100644 --- a/core/java/android/net/EthernetDataTracker.java +++ b/core/java/android/net/EthernetDataTracker.java @@ -106,6 +106,24 @@ public class EthernetDataTracker extends BaseNetworkStateTracker { mLinkCapabilities = new LinkCapabilities(); } + private void interfaceUpdated() { + // we don't get link status indications unless the iface is up - bring it up + try { + mNMService.setInterfaceUp(mIface); + String hwAddr = null; + InterfaceConfiguration config = mNMService.getInterfaceConfig(mIface); + if (config != null) { + hwAddr = config.getHardwareAddress(); + } + synchronized (this) { + mHwAddr = hwAddr; + mNetworkInfo.setExtraInfo(mHwAddr); + } + } catch (RemoteException e) { + Log.e(TAG, "Error upping interface " + mIface + ": " + e); + } + } + private void interfaceAdded(String iface) { if (!iface.matches(sIfaceMatch)) return; @@ -118,12 +136,7 @@ public class EthernetDataTracker extends BaseNetworkStateTracker { mIface = iface; } - // we don't get link status indications unless the iface is up - bring it up - try { - mNMService.setInterfaceUp(iface); - } catch (Exception e) { - Log.e(TAG, "Error upping interface " + iface + ": " + e); - } + interfaceUpdated(); mNetworkInfo.setIsAvailable(true); Message msg = mCsHandler.obtainMessage(EVENT_CONFIGURATION_CHANGED, mNetworkInfo); @@ -159,15 +172,21 @@ public class EthernetDataTracker extends BaseNetworkStateTracker { Log.d(TAG, "Removing " + iface); disconnect(); - mIface = ""; + synchronized (this) { + mIface = ""; + mHwAddr = null; + mNetworkInfo.setExtraInfo(null); + } } private void runDhcp() { Thread dhcpThread = new Thread(new Runnable() { public void run() { DhcpResults dhcpResults = new DhcpResults(); + mNetworkInfo.setDetailedState(DetailedState.OBTAINING_IPADDR, null, mHwAddr); if (!NetworkUtils.runDhcp(mIface, dhcpResults)) { Log.e(TAG, "DHCP request error:" + NetworkUtils.getDhcpError()); + mNetworkInfo.setDetailedState(DetailedState.DISCONNECTED, null, mHwAddr); return; } mLinkProperties = dhcpResults.linkProperties; @@ -220,15 +239,7 @@ public class EthernetDataTracker extends BaseNetworkStateTracker { for (String iface : ifaces) { if (iface.matches(sIfaceMatch)) { mIface = iface; - mNMService.setInterfaceUp(iface); - InterfaceConfiguration config = mNMService.getInterfaceConfig(iface); - mLinkUp = config.hasFlag("up"); - if (config != null && mHwAddr == null) { - mHwAddr = config.getHardwareAddress(); - if (mHwAddr != null) { - mNetworkInfo.setExtraInfo(mHwAddr); - } - } + interfaceUpdated(); // if a DHCP client had previously been started for this interface, then stop it NetworkUtils.stopDhcp(mIface); @@ -270,11 +281,6 @@ public class EthernetDataTracker extends BaseNetworkStateTracker { } @Override - public void captivePortalCheckComplete() { - // not implemented - } - - @Override public void captivePortalCheckCompleted(boolean isCaptivePortal) { // not implemented } @@ -423,4 +429,9 @@ public class EthernetDataTracker extends BaseNetworkStateTracker { public void supplyMessenger(Messenger messenger) { // not supported on this network } + + @Override + public String getNetworkInterfaceName() { + return mIface; + } } diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl index 4bca7fe..381a817 100644 --- a/core/java/android/net/IConnectivityManager.aidl +++ b/core/java/android/net/IConnectivityManager.aidl @@ -129,8 +129,6 @@ interface IConnectivityManager boolean updateLockdownVpn(); - void captivePortalCheckComplete(in NetworkInfo info); - void captivePortalCheckCompleted(in NetworkInfo info, boolean isCaptivePortal); void supplyMessenger(int networkType, in Messenger messenger); diff --git a/core/java/android/net/INetworkManagementEventObserver.aidl b/core/java/android/net/INetworkManagementEventObserver.aidl index 5b16f8b..dd9c39f 100644 --- a/core/java/android/net/INetworkManagementEventObserver.aidl +++ b/core/java/android/net/INetworkManagementEventObserver.aidl @@ -86,8 +86,9 @@ interface INetworkManagementEventObserver { * * @param iface The interface. * @param active True if the interface is actively transmitting data, false if it is idle. + * @param tsNanos Elapsed realtime in nanos when the state of the network interface changed. */ - void interfaceClassDataActivityChanged(String label, boolean active); + void interfaceClassDataActivityChanged(String label, boolean active, long tsNanos); /** * Information about available DNS servers has been received. diff --git a/core/java/android/net/LinkSocketNotifier.java b/core/java/android/net/LinkSocketNotifier.java index 28e2834..e2429d8 100644 --- a/core/java/android/net/LinkSocketNotifier.java +++ b/core/java/android/net/LinkSocketNotifier.java @@ -16,8 +16,6 @@ package android.net; -import java.util.Map; - /** * Interface used to get feedback about a {@link android.net.LinkSocket}. Instance is optionally * passed when a LinkSocket is constructed. Multiple LinkSockets may use the same notifier. diff --git a/core/java/android/net/MailTo.java b/core/java/android/net/MailTo.java index b90dcb1..dadb6d9 100644 --- a/core/java/android/net/MailTo.java +++ b/core/java/android/net/MailTo.java @@ -19,7 +19,6 @@ package android.net; import java.util.HashMap; import java.util.Locale; import java.util.Map; -import java.util.Set; /** * diff --git a/core/java/android/net/MobileDataStateTracker.java b/core/java/android/net/MobileDataStateTracker.java index 21352bf..a470e88 100644 --- a/core/java/android/net/MobileDataStateTracker.java +++ b/core/java/android/net/MobileDataStateTracker.java @@ -306,18 +306,18 @@ public class MobileDataStateTracker extends BaseNetworkStateTracker { if (VDBG) { Slog.d(TAG, "TelephonyMgr.DataConnectionStateChanged"); if (mNetworkInfo != null) { - Slog.d(TAG, "NetworkInfo = " + mNetworkInfo.toString()); - Slog.d(TAG, "subType = " + String.valueOf(mNetworkInfo.getSubtype())); + Slog.d(TAG, "NetworkInfo = " + mNetworkInfo); + Slog.d(TAG, "subType = " + mNetworkInfo.getSubtype()); Slog.d(TAG, "subType = " + mNetworkInfo.getSubtypeName()); } if (mLinkProperties != null) { - Slog.d(TAG, "LinkProperties = " + mLinkProperties.toString()); + Slog.d(TAG, "LinkProperties = " + mLinkProperties); } else { Slog.d(TAG, "LinkProperties = " ); } if (mLinkCapabilities != null) { - Slog.d(TAG, "LinkCapabilities = " + mLinkCapabilities.toString()); + Slog.d(TAG, "LinkCapabilities = " + mLinkCapabilities); } else { Slog.d(TAG, "LinkCapabilities = " ); } @@ -460,11 +460,6 @@ public class MobileDataStateTracker extends BaseNetworkStateTracker { } @Override - public void captivePortalCheckComplete() { - // not implemented - } - - @Override public void captivePortalCheckCompleted(boolean isCaptivePortal) { if (mIsCaptivePortal.getAndSet(isCaptivePortal) != isCaptivePortal) { // Captive portal change enable/disable failing fast diff --git a/core/java/android/net/NetworkConfig.java b/core/java/android/net/NetworkConfig.java index 5d95f41..32a2cda 100644 --- a/core/java/android/net/NetworkConfig.java +++ b/core/java/android/net/NetworkConfig.java @@ -16,7 +16,6 @@ package android.net; -import android.util.Log; import java.util.Locale; /** diff --git a/core/java/android/net/NetworkInfo.java b/core/java/android/net/NetworkInfo.java index 4d2a70d..53b1308 100644 --- a/core/java/android/net/NetworkInfo.java +++ b/core/java/android/net/NetworkInfo.java @@ -156,18 +156,20 @@ public class NetworkInfo implements Parcelable { /** {@hide} */ public NetworkInfo(NetworkInfo source) { if (source != null) { - mNetworkType = source.mNetworkType; - mSubtype = source.mSubtype; - mTypeName = source.mTypeName; - mSubtypeName = source.mSubtypeName; - mState = source.mState; - mDetailedState = source.mDetailedState; - mReason = source.mReason; - mExtraInfo = source.mExtraInfo; - mIsFailover = source.mIsFailover; - mIsRoaming = source.mIsRoaming; - mIsAvailable = source.mIsAvailable; - mIsConnectedToProvisioningNetwork = source.mIsConnectedToProvisioningNetwork; + synchronized (source) { + mNetworkType = source.mNetworkType; + mSubtype = source.mSubtype; + mTypeName = source.mTypeName; + mSubtypeName = source.mSubtypeName; + mState = source.mState; + mDetailedState = source.mDetailedState; + mReason = source.mReason; + mExtraInfo = source.mExtraInfo; + mIsFailover = source.mIsFailover; + mIsRoaming = source.mIsRoaming; + mIsAvailable = source.mIsAvailable; + mIsConnectedToProvisioningNetwork = source.mIsConnectedToProvisioningNetwork; + } } } diff --git a/core/java/android/net/NetworkKey.aidl b/core/java/android/net/NetworkKey.aidl new file mode 100644 index 0000000..637075f --- /dev/null +++ b/core/java/android/net/NetworkKey.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2014, 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.net; + +parcelable NetworkKey; diff --git a/core/java/android/net/NetworkKey.java b/core/java/android/net/NetworkKey.java new file mode 100644 index 0000000..cc3ad3e --- /dev/null +++ b/core/java/android/net/NetworkKey.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2014 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.net; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Information which identifies a specific network. + * + * @hide + */ +public class NetworkKey implements Parcelable { + + /** A wifi network, for which {@link #wifiKey} will be populated. */ + public static final int TYPE_WIFI = 1; + + /** + * The type of this network. + * @see #TYPE_WIFI + */ + public final int type; + + /** + * Information identifying a Wi-Fi network. Only set when {@link #type} equals + * {@link #TYPE_WIFI}. + */ + public final WifiKey wifiKey; + + /** + * Construct a new {@link NetworkKey} for a Wi-Fi network. + * @param wifiKey the {@link WifiKey} identifying this Wi-Fi network. + */ + public NetworkKey(WifiKey wifiKey) { + this.type = TYPE_WIFI; + this.wifiKey = wifiKey; + } + + private NetworkKey(Parcel in) { + type = in.readInt(); + switch (type) { + case TYPE_WIFI: + wifiKey = WifiKey.CREATOR.createFromParcel(in); + break; + default: + throw new IllegalArgumentException("Parcel has unknown type: " + type); + } + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(type); + switch (type) { + case TYPE_WIFI: + wifiKey.writeToParcel(out, flags); + break; + default: + throw new IllegalStateException("NetworkKey has unknown type " + type); + } + } + + @Override + public String toString() { + switch (type) { + case TYPE_WIFI: + return wifiKey.toString(); + default: + // Don't throw an exception here in case someone is logging this object in a catch + // block for debugging purposes. + return "InvalidKey"; + } + } + + public static final Parcelable.Creator<NetworkKey> CREATOR = + new Parcelable.Creator<NetworkKey>() { + @Override + public NetworkKey createFromParcel(Parcel in) { + return new NetworkKey(in); + } + + @Override + public NetworkKey[] newArray(int size) { + return new NetworkKey[size]; + } + }; +} diff --git a/core/java/android/net/NetworkScoreManager.java b/core/java/android/net/NetworkScoreManager.java new file mode 100644 index 0000000..3430547 --- /dev/null +++ b/core/java/android/net/NetworkScoreManager.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2014 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.net; + +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; +import android.content.Context; + +/** + * Class that manages communication between network subsystems and a network scorer. + * + * <p>You can get an instance of this class by calling + * {@link android.content.Context#getSystemService(String)}: + * + * <pre>NetworkScoreManager manager = + * (NetworkScoreManager) getSystemService(Context.NETWORK_SCORE_SERVICE)</pre> + * + * <p>A network scorer is any application which: + * <ul> + * <li>Declares the {@link android.Manifest.permission#SCORE_NETWORKS} permission. + * <li>Includes a receiver for {@link #ACTION_SCORE_NETWORKS} guarded by the + * {@link android.Manifest.permission#BROADCAST_SCORE_NETWORKS} permission which scores networks + * and (eventually) calls {@link #updateScores} with the results. + * </ul> + * + * <p>The system keeps track of a default scorer application; at any time, only this application + * will receive {@link #ACTION_SCORE_NETWORKS} broadcasts and will be permitted to call + * {@link #updateScores}. Applications may determine the current default scorer with + * {@link #getDefaultScorerPackage()} and request to change the default scorer by sending an + * {@link #ACTION_CHANGE_DEFAULT} broadcast with another scorer. + * + * @hide + */ +public class NetworkScoreManager { + /** + * Activity action: ask the user to change the default network scorer. This will show a dialog + * that asks the user whether they want to replace the current default scorer with the one + * specified in {@link #EXTRA_PACKAGE_NAME}. The activity will finish with RESULT_OK if the + * default was changed or RESULT_CANCELED if it failed for any reason. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_CHANGE_DEFAULT = "android.net.scoring.CHANGE_DEFAULT"; + + /** + * Extra used with {@link #ACTION_CHANGE_DEFAULT} to specify the new scorer package. Set with + * {@link android.content.Intent#putExtra(String, String)}. + */ + public static final String EXTRA_PACKAGE_NAME = "packageName"; + + /** + * Broadcast action: new network scores are being requested. This intent will only be delivered + * to the current default scorer app. That app is responsible for scoring the networks and + * calling {@link #updateScores} when complete. The networks to score are specified in + * {@link #EXTRA_NETWORKS_TO_SCORE}, and will generally consist of all networks which have been + * configured by the user as well as any open networks. + * + * <p class="note">This is a protected intent that can only be sent by the system. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_SCORE_NETWORKS = "android.net.scoring.SCORE_NETWORKS"; + + /** + * Extra used with {@link #ACTION_SCORE_NETWORKS} to specify the networks to be scored, as an + * array of {@link NetworkKey}s. Can be obtained with + * {@link android.content.Intent#getParcelableArrayExtra(String)}}. + */ + public static final String EXTRA_NETWORKS_TO_SCORE = "networksToScore"; + + private final Context mContext; + + /** @hide */ + public NetworkScoreManager(Context context) { + mContext = context; + } + + /** + * Obtain the package name of the current default network scorer. + * + * At any time, only one scorer application will receive {@link #ACTION_SCORE_NETWORKS} + * broadcasts and be allowed to call {@link #updateScores}. Applications may use this method to + * determine the current scorer and offer the user the ability to select a different scorer via + * the {@link #ACTION_CHANGE_DEFAULT} intent. + * @return the full package name of the current default scorer, or null if there is no active + * scorer. + */ + public String getDefaultScorerPackage() { + // TODO: Implement. + return null; + } + + /** + * Update network scores. + * + * This may be called at any time to re-score active networks. Scores will generally be updated + * quickly, but if this method is called too frequently, the scores may be held and applied at + * a later time. + * + * @param networks the networks which have been scored by the scorer. + * @throws SecurityException if the caller is not the default scorer. + */ + public void updateScores(ScoredNetwork[] networks) throws SecurityException { + // TODO: Implement. + } +} diff --git a/core/java/android/net/NetworkStateTracker.java b/core/java/android/net/NetworkStateTracker.java index 1ca9255..c49b1d1 100644 --- a/core/java/android/net/NetworkStateTracker.java +++ b/core/java/android/net/NetworkStateTracker.java @@ -144,11 +144,6 @@ public interface NetworkStateTracker { public boolean reconnect(); /** - * Ready to switch on to the network after captive portal check - */ - public void captivePortalCheckComplete(); - - /** * Captive portal check has completed */ public void captivePortalCheckCompleted(boolean isCaptive); diff --git a/core/java/android/net/NetworkStats.java b/core/java/android/net/NetworkStats.java index a7aae2a..54d43d3 100644 --- a/core/java/android/net/NetworkStats.java +++ b/core/java/android/net/NetworkStats.java @@ -44,6 +44,8 @@ public class NetworkStats implements Parcelable { public static final String IFACE_ALL = null; /** {@link #uid} value when UID details unavailable. */ public static final int UID_ALL = -1; + /** {@link #tag} value matching any tag. */ + public static final int TAG_ALL = -1; /** {@link #set} value when all sets combined. */ public static final int SET_ALL = -1; /** {@link #set} value where background data is accounted. */ @@ -59,8 +61,9 @@ public class NetworkStats implements Parcelable { * {@link SystemClock#elapsedRealtime()} timestamp when this data was * generated. */ - private final long elapsedRealtime; + private long elapsedRealtime; private int size; + private int capacity; private String[] iface; private int[] uid; private int[] set; @@ -152,20 +155,27 @@ public class NetworkStats implements Parcelable { public NetworkStats(long elapsedRealtime, int initialSize) { this.elapsedRealtime = elapsedRealtime; this.size = 0; - this.iface = new String[initialSize]; - this.uid = new int[initialSize]; - this.set = new int[initialSize]; - this.tag = new int[initialSize]; - this.rxBytes = new long[initialSize]; - this.rxPackets = new long[initialSize]; - this.txBytes = new long[initialSize]; - this.txPackets = new long[initialSize]; - this.operations = new long[initialSize]; + if (initialSize >= 0) { + this.capacity = initialSize; + this.iface = new String[initialSize]; + this.uid = new int[initialSize]; + this.set = new int[initialSize]; + this.tag = new int[initialSize]; + this.rxBytes = new long[initialSize]; + this.rxPackets = new long[initialSize]; + this.txBytes = new long[initialSize]; + this.txPackets = new long[initialSize]; + this.operations = new long[initialSize]; + } else { + // Special case for use by NetworkStatsFactory to start out *really* empty. + this.capacity = 0; + } } public NetworkStats(Parcel parcel) { elapsedRealtime = parcel.readLong(); size = parcel.readInt(); + capacity = parcel.readInt(); iface = parcel.createStringArray(); uid = parcel.createIntArray(); set = parcel.createIntArray(); @@ -181,6 +191,7 @@ public class NetworkStats implements Parcelable { public void writeToParcel(Parcel dest, int flags) { dest.writeLong(elapsedRealtime); dest.writeInt(size); + dest.writeInt(capacity); dest.writeStringArray(iface); dest.writeIntArray(uid); dest.writeIntArray(set); @@ -222,8 +233,8 @@ public class NetworkStats implements Parcelable { * object can be recycled across multiple calls. */ public NetworkStats addValues(Entry entry) { - if (size >= this.iface.length) { - final int newLength = Math.max(iface.length, 10) * 3 / 2; + if (size >= capacity) { + final int newLength = Math.max(size, 10) * 3 / 2; iface = Arrays.copyOf(iface, newLength); uid = Arrays.copyOf(uid, newLength); set = Arrays.copyOf(set, newLength); @@ -233,6 +244,7 @@ public class NetworkStats implements Parcelable { txBytes = Arrays.copyOf(txBytes, newLength); txPackets = Arrays.copyOf(txPackets, newLength); operations = Arrays.copyOf(operations, newLength); + capacity = newLength; } iface[size] = entry.iface; @@ -270,6 +282,10 @@ public class NetworkStats implements Parcelable { return elapsedRealtime; } + public void setElapsedRealtime(long time) { + elapsedRealtime = time; + } + /** * Return age of this {@link NetworkStats} object with respect to * {@link SystemClock#elapsedRealtime()}. @@ -284,7 +300,7 @@ public class NetworkStats implements Parcelable { @VisibleForTesting public int internalSize() { - return iface.length; + return capacity; } @Deprecated @@ -491,6 +507,17 @@ public class NetworkStats implements Parcelable { } /** + * Fast path for battery stats. + */ + public long getTotalPackets() { + long total = 0; + for (int i = size-1; i >= 0; i--) { + total += rxPackets[i] + txPackets[i]; + } + return total; + } + + /** * Subtract the given {@link NetworkStats}, effectively leaving the delta * between two snapshots in time. Assumes that statistics rows collect over * time, and that none of them have disappeared. @@ -507,8 +534,25 @@ public class NetworkStats implements Parcelable { * If counters have rolled backwards, they are clamped to {@code 0} and * reported to the given {@link NonMonotonicObserver}. */ - public static <C> NetworkStats subtract( - NetworkStats left, NetworkStats right, NonMonotonicObserver<C> observer, C cookie) { + public static <C> NetworkStats subtract(NetworkStats left, NetworkStats right, + NonMonotonicObserver<C> observer, C cookie) { + return subtract(left, right, observer, cookie, null); + } + + /** + * Subtract the two given {@link NetworkStats} objects, returning the delta + * between two snapshots in time. Assumes that statistics rows collect over + * time, and that none of them have disappeared. + * <p> + * If counters have rolled backwards, they are clamped to {@code 0} and + * reported to the given {@link NonMonotonicObserver}. + * <p> + * If <var>recycle</var> is supplied, this NetworkStats object will be + * reused (and returned) as the result if it is large enough to contain + * the data. + */ + public static <C> NetworkStats subtract(NetworkStats left, NetworkStats right, + NonMonotonicObserver<C> observer, C cookie, NetworkStats recycle) { long deltaRealtime = left.elapsedRealtime - right.elapsedRealtime; if (deltaRealtime < 0) { if (observer != null) { @@ -519,7 +563,14 @@ public class NetworkStats implements Parcelable { // result will have our rows, and elapsed time between snapshots final Entry entry = new Entry(); - final NetworkStats result = new NetworkStats(deltaRealtime, left.size); + final NetworkStats result; + if (recycle != null && recycle.capacity >= left.size) { + result = recycle; + result.size = 0; + result.elapsedRealtime = deltaRealtime; + } else { + result = new NetworkStats(deltaRealtime, left.size); + } for (int i = 0; i < left.size; i++) { entry.iface = left.iface[i]; entry.uid = left.uid[i]; diff --git a/core/java/android/net/Proxy.java b/core/java/android/net/Proxy.java index c3e1438..033332c 100644 --- a/core/java/android/net/Proxy.java +++ b/core/java/android/net/Proxy.java @@ -66,6 +66,19 @@ public final class Proxy { /** {@hide} **/ public static final String EXTRA_PROXY_INFO = "proxy"; + /** @hide */ + public static final int PROXY_VALID = 0; + /** @hide */ + public static final int PROXY_HOSTNAME_EMPTY = 1; + /** @hide */ + public static final int PROXY_HOSTNAME_INVALID = 2; + /** @hide */ + public static final int PROXY_PORT_EMPTY = 3; + /** @hide */ + public static final int PROXY_PORT_INVALID = 4; + /** @hide */ + public static final int PROXY_EXCLLIST_INVALID = 5; + private static ConnectivityManager sConnectivityManager = null; // Hostname / IP REGEX validation @@ -77,8 +90,10 @@ public final class Proxy { private static final Pattern HOSTNAME_PATTERN; - private static final String EXCLLIST_REGEXP = "$|^(.?" + NAME_IP_REGEX - + ")+(,(.?" + NAME_IP_REGEX + "))*$"; + private static final String EXCL_REGEX = + "[a-zA-Z0-9*]+(\\-[a-zA-Z0-9*]+)*(\\.[a-zA-Z0-9*]+(\\-[a-zA-Z0-9*]+)*)*"; + + private static final String EXCLLIST_REGEXP = "^$|^" + EXCL_REGEX + "(," + EXCL_REGEX + ")*$"; private static final Pattern EXCLLIST_PATTERN; @@ -236,36 +251,27 @@ public final class Proxy { * Validate syntax of hostname, port and exclusion list entries * {@hide} */ - public static void validate(String hostname, String port, String exclList) { + public static int validate(String hostname, String port, String exclList) { Matcher match = HOSTNAME_PATTERN.matcher(hostname); Matcher listMatch = EXCLLIST_PATTERN.matcher(exclList); - if (!match.matches()) { - throw new IllegalArgumentException(); - } + if (!match.matches()) return PROXY_HOSTNAME_INVALID; - if (!listMatch.matches()) { - throw new IllegalArgumentException(); - } + if (!listMatch.matches()) return PROXY_EXCLLIST_INVALID; - if (hostname.length() > 0 && port.length() == 0) { - throw new IllegalArgumentException(); - } + if (hostname.length() > 0 && port.length() == 0) return PROXY_PORT_EMPTY; if (port.length() > 0) { - if (hostname.length() == 0) { - throw new IllegalArgumentException(); - } + if (hostname.length() == 0) return PROXY_HOSTNAME_EMPTY; int portVal = -1; try { portVal = Integer.parseInt(port); } catch (NumberFormatException ex) { - throw new IllegalArgumentException(); - } - if (portVal <= 0 || portVal > 0xFFFF) { - throw new IllegalArgumentException(); + return PROXY_PORT_INVALID; } + if (portVal <= 0 || portVal > 0xFFFF) return PROXY_PORT_INVALID; } + return PROXY_VALID; } static class AndroidProxySelectorRoutePlanner diff --git a/core/java/android/net/ProxyProperties.java b/core/java/android/net/ProxyProperties.java index 010e527..50f45e8 100644 --- a/core/java/android/net/ProxyProperties.java +++ b/core/java/android/net/ProxyProperties.java @@ -22,7 +22,6 @@ import android.os.Parcelable; import android.text.TextUtils; import java.net.InetSocketAddress; -import java.net.UnknownHostException; import java.util.Locale; /** @@ -141,13 +140,9 @@ public class ProxyProperties implements Parcelable { public boolean isValid() { if (!TextUtils.isEmpty(mPacFileUrl)) return true; - try { - Proxy.validate(mHost == null ? "" : mHost, mPort == 0 ? "" : Integer.toString(mPort), - mExclusionList == null ? "" : mExclusionList); - } catch (IllegalArgumentException e) { - return false; - } - return true; + return Proxy.PROXY_VALID == Proxy.validate(mHost == null ? "" : mHost, + mPort == 0 ? "" : Integer.toString(mPort), + mExclusionList == null ? "" : mExclusionList); } public java.net.Proxy makeProxy() { diff --git a/core/java/android/net/RssiCurve.java b/core/java/android/net/RssiCurve.java new file mode 100644 index 0000000..7af7998 --- /dev/null +++ b/core/java/android/net/RssiCurve.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2014 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.net; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * A curve defining the network score over a range of RSSI values. + * + * <p>For each RSSI bucket, the score may be any byte. Scores have no absolute meaning and are only + * considered relative to other scores assigned by the same scorer. Networks with no score are all + * considered equivalent and ranked below any network with a score. + * + * <p>For example, consider a curve starting at -110 dBm with a bucket width of 10 and the + * following buckets: {@code [-20, -10, 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120]}. + * This represents a linear curve between -110 dBm and 30 dBm. It scores progressively higher at + * stronger signal strengths. + * + * <p>A network can be assigned a fixed score independent of RSSI by setting + * {@link #rssiBuckets} to a one-byte array whose element is the fixed score. {@link #start} + * should be set to the lowest RSSI value at which this fixed score should apply, and + * {@link #bucketWidth} should be set such that {@code start + bucketWidth} is equal to the + * highest RSSI value at which this fixed score should apply. + * + * <p>Note that RSSI values below -110 dBm or above 30 dBm are unlikely to cause any difference + * in connectivity behavior from those endpoints. That is, the connectivity framework will treat + * a network with a -120 dBm signal exactly as it would treat one with a -110 dBm signal. + * Therefore, graphs which specify scores outside this range may be truncated to this range by + * the system. + * + * @see ScoredNetwork + * @hide + */ +public class RssiCurve implements Parcelable { + + /** The starting dBm of the curve. */ + public final int start; + + /** The width of each RSSI bucket, in dBm. */ + public final int bucketWidth; + + /** The score for each RSSI bucket. */ + public final byte[] rssiBuckets; + + /** + * Construct a new {@link RssiCurve}. + * + * @param start the starting dBm of the curve. + * @param bucketWidth the width of each RSSI bucket, in dBm. + * @param rssiBuckets the score for each RSSI bucket. + */ + public RssiCurve(int start, int bucketWidth, byte[] rssiBuckets) { + this.start = start; + this.bucketWidth = bucketWidth; + if (rssiBuckets == null || rssiBuckets.length == 0) { + throw new IllegalArgumentException("rssiBuckets must be at least one element large."); + } + this.rssiBuckets = rssiBuckets; + } + + private RssiCurve(Parcel in) { + start = in.readInt(); + bucketWidth = in.readInt(); + int bucketCount = in.readInt(); + rssiBuckets = new byte[bucketCount]; + in.readByteArray(rssiBuckets); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(start); + out.writeInt(bucketWidth); + out.writeInt(rssiBuckets.length); + out.writeByteArray(rssiBuckets); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("RssiCurve[start=") + .append(start) + .append(",bucketWidth=") + .append(bucketWidth); + + sb.append(",buckets="); + for (int i = 0; i < rssiBuckets.length; i++) { + sb.append(rssiBuckets[i]); + if (i < rssiBuckets.length - 1) { + sb.append(","); + } + } + sb.append("]"); + + return sb.toString(); + } + + public static final Creator<RssiCurve> CREATOR = + new Creator<RssiCurve>() { + @Override + public RssiCurve createFromParcel(Parcel in) { + return new RssiCurve(in); + } + + @Override + public RssiCurve[] newArray(int size) { + return new RssiCurve[size]; + } + }; +} diff --git a/core/java/android/net/SSLCertificateSocketFactory.java b/core/java/android/net/SSLCertificateSocketFactory.java index b0278d3..12e8791 100644 --- a/core/java/android/net/SSLCertificateSocketFactory.java +++ b/core/java/android/net/SSLCertificateSocketFactory.java @@ -135,7 +135,8 @@ public class SSLCertificateSocketFactory extends SSLSocketFactory { * disabled, using an optional handshake timeout and SSL session cache. * * <p class="caution"><b>Warning:</b> Sockets created using this factory - * are vulnerable to man-in-the-middle attacks!</p> + * are vulnerable to man-in-the-middle attacks!</p>. The caller must implement + * its own verification. * * @param handshakeTimeoutMillis to use for SSL connection handshake, or 0 * for none. The socket timeout is reset to 0 after the handshake. @@ -223,8 +224,6 @@ public class SSLCertificateSocketFactory extends SSLSocketFactory { if (mInsecureFactory == null) { if (mSecure) { Log.w(TAG, "*** BYPASSING SSL SECURITY CHECKS (socket.relaxsslcheck=yes) ***"); - } else { - Log.w(TAG, "Bypassing SSL security checks at caller's request"); } mInsecureFactory = makeSocketFactory(mKeyManagers, INSECURE_TRUST_MANAGER); } @@ -431,6 +430,7 @@ public class SSLCertificateSocketFactory extends SSLSocketFactory { s.setAlpnProtocols(mAlpnProtocols); s.setHandshakeTimeout(mHandshakeTimeoutMillis); s.setChannelIdPrivateKey(mChannelIdPrivateKey); + s.setHostname(host); if (mSecure) { verifyHostname(s, host); } diff --git a/core/java/android/net/SSLSessionCache.java b/core/java/android/net/SSLSessionCache.java index 15421de..65db2c3 100644 --- a/core/java/android/net/SSLSessionCache.java +++ b/core/java/android/net/SSLSessionCache.java @@ -19,12 +19,16 @@ package android.net; import android.content.Context; import android.util.Log; +import com.android.org.conscrypt.ClientSessionContext; import com.android.org.conscrypt.FileClientSessionCache; import com.android.org.conscrypt.SSLClientSessionCache; import java.io.File; import java.io.IOException; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSessionContext; + /** * File-based cache of established SSL sessions. When re-establishing a * connection to the same server, using an SSL session cache can save some time, @@ -38,6 +42,40 @@ public final class SSLSessionCache { /* package */ final SSLClientSessionCache mSessionCache; /** + * Installs a {@link SSLSessionCache} on a {@link SSLContext}. The cache will + * be used on all socket factories created by this context (including factories + * created before this call). + * + * @param cache the cache instance to install, or {@code null} to uninstall any + * existing cache. + * @param context the context to install it on. + * @throws IllegalArgumentException if the context does not support a session + * cache. + * + * @hide candidate for public API + */ + public static void install(SSLSessionCache cache, SSLContext context) { + SSLSessionContext clientContext = context.getClientSessionContext(); + if (clientContext instanceof ClientSessionContext) { + ((ClientSessionContext) clientContext).setPersistentCache( + cache == null ? null : cache.mSessionCache); + } else { + throw new IllegalArgumentException("Incompatible SSLContext: " + context); + } + } + + /** + * NOTE: This needs to be Object (and not SSLClientSessionCache) because apps + * that build directly against the framework (and not the SDK) might not declare + * a dependency on conscrypt. Javac will then has fail while resolving constructors. + * + * @hide For unit test use only + */ + public SSLSessionCache(Object cache) { + mSessionCache = (SSLClientSessionCache) cache; + } + + /** * Create a session cache using the specified directory. * Individual session entries will be files within the directory. * Multiple instances for the same directory share data internally. diff --git a/core/java/android/net/ScoredNetwork.aidl b/core/java/android/net/ScoredNetwork.aidl new file mode 100644 index 0000000..f83db11 --- /dev/null +++ b/core/java/android/net/ScoredNetwork.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2014, 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.net; + +parcelable ScoredNetwork; diff --git a/core/java/android/net/ScoredNetwork.java b/core/java/android/net/ScoredNetwork.java new file mode 100644 index 0000000..8af3c3c --- /dev/null +++ b/core/java/android/net/ScoredNetwork.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2014 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.net; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * A network identifier along with a score for the quality of that network. + * + * @hide + */ +public class ScoredNetwork implements Parcelable { + + /** A {@link NetworkKey} uniquely identifying this network. */ + public final NetworkKey networkKey; + + /** + * The {@link RssiCurve} representing the scores for this network based on the RSSI. + * + * <p>This field is optional and may be set to null to indicate that no score is available for + * this network at this time. Such networks, along with networks for which the scorer has not + * responded, are always prioritized below scored networks, regardless of the score. + */ + public final RssiCurve rssiCurve; + + /** + * Construct a new {@link ScoredNetwork}. + * + * @param networkKey the {@link NetworkKey} uniquely identifying this network. + * @param rssiCurve the {@link RssiCurve} representing the scores for this network based on the + * RSSI. This field is optional, and may be skipped to represent a network which the scorer + * has opted not to score at this time. Passing a null value here is strongly preferred to + * not returning any {@link ScoredNetwork} for a given {@link NetworkKey} because it + * indicates to the system not to request scores for this network in the future, although + * the scorer may choose to issue an out-of-band update at any time. + */ + public ScoredNetwork(NetworkKey networkKey, RssiCurve rssiCurve) { + this.networkKey = networkKey; + this.rssiCurve = rssiCurve; + } + + private ScoredNetwork(Parcel in) { + networkKey = NetworkKey.CREATOR.createFromParcel(in); + if (in.readByte() == 1) { + rssiCurve = RssiCurve.CREATOR.createFromParcel(in); + } else { + rssiCurve = null; + } + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + networkKey.writeToParcel(out, flags); + if (rssiCurve != null) { + out.writeByte((byte) 1); + rssiCurve.writeToParcel(out, flags); + } else { + out.writeByte((byte) 0); + } + } + + @Override + public String toString() { + return "ScoredNetwork[key=" + networkKey + ",score=" + rssiCurve + "]"; + } + + public static final Parcelable.Creator<ScoredNetwork> CREATOR = + new Parcelable.Creator<ScoredNetwork>() { + @Override + public ScoredNetwork createFromParcel(Parcel in) { + return new ScoredNetwork(in); + } + + @Override + public ScoredNetwork[] newArray(int size) { + return new ScoredNetwork[size]; + } + }; +} diff --git a/core/java/android/net/SntpClient.java b/core/java/android/net/SntpClient.java index 316440f..7673011 100644 --- a/core/java/android/net/SntpClient.java +++ b/core/java/android/net/SntpClient.java @@ -19,7 +19,6 @@ package android.net; import android.os.SystemClock; import android.util.Log; -import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; diff --git a/core/java/android/net/WifiKey.java b/core/java/android/net/WifiKey.java new file mode 100644 index 0000000..ffcd85a --- /dev/null +++ b/core/java/android/net/WifiKey.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2014 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.net; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.regex.Pattern; + +/** + * Information identifying a Wi-Fi network. + * @see NetworkKey + * + * @hide + */ +public class WifiKey implements Parcelable { + + // Patterns used for validation. + private static final Pattern SSID_PATTERN = Pattern.compile("(\".*\")|(0x[\\p{XDigit}]+)"); + private static final Pattern BSSID_PATTERN = + Pattern.compile("([\\p{XDigit}]{2}:){5}[\\p{XDigit}]{2}"); + + /** + * The service set identifier (SSID) of an 802.11 network. If the SSID can be decoded as + * UTF-8, it will be surrounded by double quotation marks. Otherwise, it will be a string of + * hex digits starting with 0x. + */ + public final String ssid; + + /** + * The basic service set identifier (BSSID) of an access point for this network. This will + * be in the form of a six-byte MAC address: {@code XX:XX:XX:XX:XX:XX}, where each X is a + * hexadecimal digit. + */ + public final String bssid; + + /** + * Construct a new {@link WifiKey} for the given Wi-Fi SSID/BSSID pair. + * + * @param ssid the service set identifier (SSID) of an 802.11 network. If the SSID can be + * decoded as UTF-8, it should be surrounded by double quotation marks. Otherwise, + * it should be a string of hex digits starting with 0x. + * @param bssid the basic service set identifier (BSSID) of this network's access point. + * This should be in the form of a six-byte MAC address: {@code XX:XX:XX:XX:XX:XX}, + * where each X is a hexadecimal digit. + * @throws IllegalArgumentException if either the SSID or BSSID is invalid. + */ + public WifiKey(String ssid, String bssid) { + if (!SSID_PATTERN.matcher(ssid).matches()) { + throw new IllegalArgumentException("Invalid ssid: " + ssid); + } + if (!BSSID_PATTERN.matcher(bssid).matches()) { + throw new IllegalArgumentException("Invalid bssid: " + bssid); + } + this.ssid = ssid; + this.bssid = bssid; + } + + private WifiKey(Parcel in) { + ssid = in.readString(); + bssid = in.readString(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeString(ssid); + out.writeString(bssid); + } + + @Override + public String toString() { + return "WifiKey[SSID=" + ssid + ",BSSID=" + bssid + "]"; + } + + public static final Creator<WifiKey> CREATOR = + new Creator<WifiKey>() { + @Override + public WifiKey createFromParcel(Parcel in) { + return new WifiKey(in); + } + + @Override + public WifiKey[] newArray(int size) { + return new WifiKey[size]; + } + }; +} diff --git a/core/java/android/net/dhcp/DhcpAckPacket.java b/core/java/android/net/dhcp/DhcpAckPacket.java index 4eca531..7b8be9c 100644 --- a/core/java/android/net/dhcp/DhcpAckPacket.java +++ b/core/java/android/net/dhcp/DhcpAckPacket.java @@ -19,7 +19,6 @@ package android.net.dhcp; import java.net.InetAddress; import java.net.Inet4Address; import java.nio.ByteBuffer; -import java.util.List; /** * This class implements the DHCP-ACK packet. diff --git a/core/java/android/net/dhcp/DhcpOfferPacket.java b/core/java/android/net/dhcp/DhcpOfferPacket.java index 3d79f4d..f1c30e1 100644 --- a/core/java/android/net/dhcp/DhcpOfferPacket.java +++ b/core/java/android/net/dhcp/DhcpOfferPacket.java @@ -19,7 +19,6 @@ package android.net.dhcp; import java.net.InetAddress; import java.net.Inet4Address; import java.nio.ByteBuffer; -import java.util.List; /** * This class implements the DHCP-OFFER packet. diff --git a/core/java/android/net/dhcp/DhcpPacket.java b/core/java/android/net/dhcp/DhcpPacket.java index 317a9b4..c7c25f0 100644 --- a/core/java/android/net/dhcp/DhcpPacket.java +++ b/core/java/android/net/dhcp/DhcpPacket.java @@ -1,8 +1,5 @@ package android.net.dhcp; -import android.util.Log; - -import java.io.UnsupportedEncodingException; import java.net.InetAddress; import java.net.UnknownHostException; import java.nio.ByteBuffer; diff --git a/core/java/android/net/dhcp/DhcpStateMachine.java b/core/java/android/net/dhcp/DhcpStateMachine.java index b6c384d..bc9a798 100644 --- a/core/java/android/net/dhcp/DhcpStateMachine.java +++ b/core/java/android/net/dhcp/DhcpStateMachine.java @@ -17,7 +17,6 @@ package android.net.dhcp; import java.net.InetAddress; -import java.nio.ByteBuffer; import java.util.List; /** diff --git a/core/java/android/net/http/AndroidHttpClientConnection.java b/core/java/android/net/http/AndroidHttpClientConnection.java index eb96679..6d48fce 100644 --- a/core/java/android/net/http/AndroidHttpClientConnection.java +++ b/core/java/android/net/http/AndroidHttpClientConnection.java @@ -16,8 +16,6 @@ package android.net.http; -import org.apache.http.Header; - import org.apache.http.HttpConnection; import org.apache.http.HttpClientConnection; import org.apache.http.HttpConnectionMetrics; @@ -27,12 +25,10 @@ import org.apache.http.HttpException; import org.apache.http.HttpInetConnection; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; -import org.apache.http.HttpResponseFactory; import org.apache.http.NoHttpResponseException; import org.apache.http.StatusLine; import org.apache.http.entity.BasicHttpEntity; import org.apache.http.entity.ContentLengthStrategy; -import org.apache.http.impl.DefaultHttpResponseFactory; import org.apache.http.impl.HttpConnectionMetricsImpl; import org.apache.http.impl.entity.EntitySerializer; import org.apache.http.impl.entity.StrictContentLengthStrategy; diff --git a/core/java/android/net/http/Connection.java b/core/java/android/net/http/Connection.java index 95cecd2..834ad69 100644 --- a/core/java/android/net/http/Connection.java +++ b/core/java/android/net/http/Connection.java @@ -21,7 +21,6 @@ import android.os.SystemClock; import java.io.IOException; import java.net.UnknownHostException; -import java.util.ListIterator; import java.util.LinkedList; import javax.net.ssl.SSLHandshakeException; diff --git a/core/java/android/net/http/ConnectionThread.java b/core/java/android/net/http/ConnectionThread.java index 32191d2..d825530 100644 --- a/core/java/android/net/http/ConnectionThread.java +++ b/core/java/android/net/http/ConnectionThread.java @@ -19,8 +19,6 @@ package android.net.http; import android.content.Context; import android.os.SystemClock; -import org.apache.http.HttpHost; - import java.lang.Thread; /** diff --git a/core/java/android/net/http/HttpConnection.java b/core/java/android/net/http/HttpConnection.java index 6df86bf..edf8fed3 100644 --- a/core/java/android/net/http/HttpConnection.java +++ b/core/java/android/net/http/HttpConnection.java @@ -21,9 +21,7 @@ import android.content.Context; import java.net.Socket; import java.io.IOException; -import org.apache.http.HttpClientConnection; import org.apache.http.HttpHost; -import org.apache.http.impl.DefaultHttpClientConnection; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.HttpConnectionParams; diff --git a/core/java/android/net/http/HttpResponseCache.java b/core/java/android/net/http/HttpResponseCache.java index 269dfb8..2785a15 100644 --- a/core/java/android/net/http/HttpResponseCache.java +++ b/core/java/android/net/http/HttpResponseCache.java @@ -17,9 +17,6 @@ package android.net.http; import android.content.Context; -import com.android.okhttp.OkResponseCache; -import com.android.okhttp.ResponseSource; -import com.android.okhttp.internal.DiskLruCache; import java.io.Closeable; import java.io.File; import java.io.IOException; @@ -32,7 +29,6 @@ import java.net.URLConnection; import java.util.List; import java.util.Map; import javax.net.ssl.HttpsURLConnection; -import libcore.io.IoUtils; import org.apache.http.impl.client.DefaultHttpClient; /** diff --git a/core/java/android/net/http/HttpsConnection.java b/core/java/android/net/http/HttpsConnection.java index 7a12e53..6bf01e2 100644 --- a/core/java/android/net/http/HttpsConnection.java +++ b/core/java/android/net/http/HttpsConnection.java @@ -40,7 +40,6 @@ import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import java.io.File; import java.io.IOException; -import java.net.InetSocketAddress; import java.net.Socket; import java.security.KeyManagementException; import java.security.cert.X509Certificate; diff --git a/core/java/android/net/http/Request.java b/core/java/android/net/http/Request.java index 8c0d503..76d7bb9 100644 --- a/core/java/android/net/http/Request.java +++ b/core/java/android/net/http/Request.java @@ -26,15 +26,12 @@ import java.util.zip.GZIPInputStream; import org.apache.http.entity.InputStreamEntity; import org.apache.http.Header; -import org.apache.http.HttpClientConnection; import org.apache.http.HttpEntity; import org.apache.http.HttpEntityEnclosingRequest; import org.apache.http.HttpException; import org.apache.http.HttpHost; import org.apache.http.HttpRequest; -import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; -import org.apache.http.HttpVersion; import org.apache.http.ParseException; import org.apache.http.ProtocolVersion; diff --git a/core/java/android/net/http/RequestQueue.java b/core/java/android/net/http/RequestQueue.java index ce6b1ad..7d2da1b 100644 --- a/core/java/android/net/http/RequestQueue.java +++ b/core/java/android/net/http/RequestQueue.java @@ -29,10 +29,6 @@ import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Proxy; import android.net.WebAddress; -import android.os.Handler; -import android.os.Message; -import android.os.SystemProperties; -import android.text.TextUtils; import android.util.Log; import java.io.InputStream; diff --git a/core/java/android/net/http/X509TrustManagerExtensions.java b/core/java/android/net/http/X509TrustManagerExtensions.java index d730a7b..830ddce 100644 --- a/core/java/android/net/http/X509TrustManagerExtensions.java +++ b/core/java/android/net/http/X509TrustManagerExtensions.java @@ -76,4 +76,18 @@ public class X509TrustManagerExtensions { return mDelegate.checkServerTrusted(chain, authType, new DelegatingSSLSession.HostnameWrap(host)); } + + /** + * Checks whether a CA certificate is added by an user. + * + * <p>Since {@link X509TrustManager#checkServerTrusted} allows its parameter {@code chain} to + * chain up to user-added CA certificates, this method can be used to perform additional + * policies for user-added CA certificates. + * + * @return {@code true} to indicate that the certificate was added by the user, {@code false} + * otherwise. + */ + public boolean isUserAddedCertificate(X509Certificate cert) { + return mDelegate.isUserAddedCertificate(cert); + } } diff --git a/core/java/android/net/nsd/NsdManager.java b/core/java/android/net/nsd/NsdManager.java index 7b2c623..377ed88 100644 --- a/core/java/android/net/nsd/NsdManager.java +++ b/core/java/android/net/nsd/NsdManager.java @@ -19,8 +19,6 @@ package android.net.nsd; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.content.Context; -import android.os.Binder; -import android.os.IBinder; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; @@ -213,6 +211,7 @@ public final class NsdManager { private Context mContext; private static final int INVALID_LISTENER_KEY = 0; + private static final int BUSY_LISTENER_KEY = -1; private int mListenerKey = 1; private final SparseArray mListenerMap = new SparseArray(); private final SparseArray<NsdServiceInfo> mServiceMap = new SparseArray<NsdServiceInfo>(); @@ -319,71 +318,74 @@ public final class NsdManager { Log.d(TAG, "Stale key " + message.arg2); return; } - boolean listenerRemove = true; NsdServiceInfo ns = getNsdService(message.arg2); switch (message.what) { case DISCOVER_SERVICES_STARTED: String s = getNsdServiceInfoType((NsdServiceInfo) message.obj); ((DiscoveryListener) listener).onDiscoveryStarted(s); - // Keep listener until stop discovery - listenerRemove = false; break; case DISCOVER_SERVICES_FAILED: + removeListener(message.arg2); ((DiscoveryListener) listener).onStartDiscoveryFailed(getNsdServiceInfoType(ns), message.arg1); break; case SERVICE_FOUND: ((DiscoveryListener) listener).onServiceFound((NsdServiceInfo) message.obj); - // Keep listener until stop discovery - listenerRemove = false; break; case SERVICE_LOST: ((DiscoveryListener) listener).onServiceLost((NsdServiceInfo) message.obj); - // Keep listener until stop discovery - listenerRemove = false; break; case STOP_DISCOVERY_FAILED: + removeListener(message.arg2); ((DiscoveryListener) listener).onStopDiscoveryFailed(getNsdServiceInfoType(ns), message.arg1); break; case STOP_DISCOVERY_SUCCEEDED: + removeListener(message.arg2); ((DiscoveryListener) listener).onDiscoveryStopped(getNsdServiceInfoType(ns)); break; case REGISTER_SERVICE_FAILED: + removeListener(message.arg2); ((RegistrationListener) listener).onRegistrationFailed(ns, message.arg1); break; case REGISTER_SERVICE_SUCCEEDED: ((RegistrationListener) listener).onServiceRegistered( (NsdServiceInfo) message.obj); - // Keep listener until unregister - listenerRemove = false; break; case UNREGISTER_SERVICE_FAILED: + removeListener(message.arg2); ((RegistrationListener) listener).onUnregistrationFailed(ns, message.arg1); break; case UNREGISTER_SERVICE_SUCCEEDED: + removeListener(message.arg2); ((RegistrationListener) listener).onServiceUnregistered(ns); break; case RESOLVE_SERVICE_FAILED: + removeListener(message.arg2); ((ResolveListener) listener).onResolveFailed(ns, message.arg1); break; case RESOLVE_SERVICE_SUCCEEDED: + removeListener(message.arg2); ((ResolveListener) listener).onServiceResolved((NsdServiceInfo) message.obj); break; default: Log.d(TAG, "Ignored " + message); break; } - if (listenerRemove) { - removeListener(message.arg2); - } } } + // if the listener is already in the map, reject it. Otherwise, add it and + // return its key. + private int putListener(Object listener, NsdServiceInfo s) { if (listener == null) return INVALID_LISTENER_KEY; int key; synchronized (mMapLock) { + int valueIndex = mListenerMap.indexOfValue(listener); + if (valueIndex != -1) { + return BUSY_LISTENER_KEY; + } do { key = mListenerKey++; } while (key == INVALID_LISTENER_KEY); @@ -424,7 +426,6 @@ public final class NsdManager { return INVALID_LISTENER_KEY; } - private String getNsdServiceInfoType(NsdServiceInfo s) { if (s == null) return "?"; return s.getServiceType(); @@ -451,14 +452,18 @@ public final class NsdManager { * Register a service to be discovered by other services. * * <p> The function call immediately returns after sending a request to register service - * to the framework. The application is notified of a success to initiate - * discovery through the callback {@link RegistrationListener#onServiceRegistered} or a failure + * to the framework. The application is notified of a successful registration + * through the callback {@link RegistrationListener#onServiceRegistered} or a failure * through {@link RegistrationListener#onRegistrationFailed}. * + * <p> The application should call {@link #unregisterService} when the service + * registration is no longer required, and/or whenever the application is stopped. + * * @param serviceInfo The service being registered * @param protocolType The service discovery protocol * @param listener The listener notifies of a successful registration and is used to * unregister this service through a call on {@link #unregisterService}. Cannot be null. + * Cannot be in use for an active service registration. */ public void registerService(NsdServiceInfo serviceInfo, int protocolType, RegistrationListener listener) { @@ -475,8 +480,11 @@ public final class NsdManager { if (protocolType != PROTOCOL_DNS_SD) { throw new IllegalArgumentException("Unsupported protocol"); } - mAsyncChannel.sendMessage(REGISTER_SERVICE, 0, putListener(listener, serviceInfo), - serviceInfo); + int key = putListener(listener, serviceInfo); + if (key == BUSY_LISTENER_KEY) { + throw new IllegalArgumentException("listener already in use"); + } + mAsyncChannel.sendMessage(REGISTER_SERVICE, 0, key, serviceInfo); } /** @@ -486,7 +494,11 @@ public final class NsdManager { * * @param listener This should be the listener object that was passed to * {@link #registerService}. It identifies the service that should be unregistered - * and notifies of a successful unregistration. + * and notifies of a successful or unsuccessful unregistration via the listener + * callbacks. In API versions 20 and above, the listener object may be used for + * another service registration once the callback has been called. In API versions <= 19, + * there is no entirely reliable way to know when a listener may be re-used, and a new + * listener should be created for each service registration request. */ public void unregisterService(RegistrationListener listener) { int id = getListenerKey(listener); @@ -516,12 +528,16 @@ public final class NsdManager { * <p> Upon failure to start, service discovery is not active and application does * not need to invoke {@link #stopServiceDiscovery} * + * <p> The application should call {@link #stopServiceDiscovery} when discovery of this + * service type is no longer required, and/or whenever the application is paused or + * stopped. + * * @param serviceType The service type being discovered. Examples include "_http._tcp" for * http services or "_ipp._tcp" for printers * @param protocolType The service discovery protocol * @param listener The listener notifies of a successful discovery and is used * to stop discovery on this serviceType through a call on {@link #stopServiceDiscovery}. - * Cannot be null. + * Cannot be null. Cannot be in use for an active service discovery. */ public void discoverServices(String serviceType, int protocolType, DiscoveryListener listener) { if (listener == null) { @@ -537,11 +553,17 @@ public final class NsdManager { NsdServiceInfo s = new NsdServiceInfo(); s.setServiceType(serviceType); - mAsyncChannel.sendMessage(DISCOVER_SERVICES, 0, putListener(listener, s), s); + + int key = putListener(listener, s); + if (key == BUSY_LISTENER_KEY) { + throw new IllegalArgumentException("listener already in use"); + } + + mAsyncChannel.sendMessage(DISCOVER_SERVICES, 0, key, s); } /** - * Stop service discovery initiated with {@link #discoverServices}. An active service + * Stop service discovery initiated with {@link #discoverServices}. An active service * discovery is notified to the application with {@link DiscoveryListener#onDiscoveryStarted} * and it stays active until the application invokes a stop service discovery. A successful * stop is notified to with a call to {@link DiscoveryListener#onDiscoveryStopped}. @@ -550,7 +572,11 @@ public final class NsdManager { * {@link DiscoveryListener#onStopDiscoveryFailed}. * * @param listener This should be the listener object that was passed to {@link #discoverServices}. - * It identifies the discovery that should be stopped and notifies of a successful stop. + * It identifies the discovery that should be stopped and notifies of a successful or + * unsuccessful stop. In API versions 20 and above, the listener object may be used for + * another service discovery once the callback has been called. In API versions <= 19, + * there is no entirely reliable way to know when a listener may be re-used, and a new + * listener should be created for each service discovery request. */ public void stopServiceDiscovery(DiscoveryListener listener) { int id = getListenerKey(listener); @@ -570,6 +596,7 @@ public final class NsdManager { * * @param serviceInfo service to be resolved * @param listener to receive callback upon success or failure. Cannot be null. + * Cannot be in use for an active service resolution. */ public void resolveService(NsdServiceInfo serviceInfo, ResolveListener listener) { if (TextUtils.isEmpty(serviceInfo.getServiceName()) || @@ -579,8 +606,13 @@ public final class NsdManager { if (listener == null) { throw new IllegalArgumentException("listener cannot be null"); } - mAsyncChannel.sendMessage(RESOLVE_SERVICE, 0, putListener(listener, serviceInfo), - serviceInfo); + + int key = putListener(listener, serviceInfo); + + if (key == BUSY_LISTENER_KEY) { + throw new IllegalArgumentException("listener already in use"); + } + mAsyncChannel.sendMessage(RESOLVE_SERVICE, 0, key, serviceInfo); } /** Internal use only @hide */ diff --git a/core/java/android/nfc/INfcAdapter.aidl b/core/java/android/nfc/INfcAdapter.aidl index 8414738..635a50f 100644 --- a/core/java/android/nfc/INfcAdapter.aidl +++ b/core/java/android/nfc/INfcAdapter.aidl @@ -25,6 +25,7 @@ import android.nfc.IAppCallback; import android.nfc.INfcAdapterExtras; import android.nfc.INfcTag; import android.nfc.INfcCardEmulation; +import android.nfc.INfcUnlockSettings; import android.os.Bundle; /** @@ -35,6 +36,7 @@ interface INfcAdapter INfcTag getNfcTagInterface(); INfcCardEmulation getNfcCardEmulationInterface(); INfcAdapterExtras getNfcAdapterExtrasInterface(in String pkg); + INfcUnlockSettings getNfcUnlockSettingsInterface(); int getState(); boolean disable(boolean saveState); @@ -46,6 +48,7 @@ interface INfcAdapter void setForegroundDispatch(in PendingIntent intent, in IntentFilter[] filters, in TechListParcel techLists); void setAppCallback(in IAppCallback callback); + void invokeBeam(); void dispatch(in Tag tag); diff --git a/core/java/android/nfc/INfcUnlockSettings.aidl b/core/java/android/nfc/INfcUnlockSettings.aidl new file mode 100644 index 0000000..649eeed --- /dev/null +++ b/core/java/android/nfc/INfcUnlockSettings.aidl @@ -0,0 +1,70 @@ +/* + * 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; + +import android.nfc.Tag; +import java.util.List; + +/** + * Interface to NFC unlock functionality. + * + * @hide + */ +interface INfcUnlockSettings { + + /** + * Checks the validity of the tag and attempts to unlock the screen. + * + * @return true if the screen was successfuly unlocked. + */ + boolean tryUnlock(int userId, in Tag tag); + + /** + * Registers the given tag as an unlock tag. Subsequent calls to {@code tryUnlock} + * with the same {@code tag} should succeed. + * + * @return true if the tag was successfully registered. + */ + boolean registerTag(int userId, in Tag tag); + + /** + * Deregisters the tag with the corresponding timestamp. + * Subsequent calls to {@code tryUnlock} with the same tag should fail. + * + * @return true if the tag was successfully deleted. + */ + boolean deregisterTag(int userId, long timestamp); + + /** + * Used for user-visible rendering of registered tags. + * + * @return a list of the times in millis since epoch when the registered tags were paired. + */ + long[] getTagRegistryTimes(int userId); + + /** + * Determines the state of the NFC unlock feature. + * + * @return true if NFC unlock is enabled. + */ + boolean getNfcUnlockEnabled(int userId); + + /** + * Sets the state [ON | OFF] of the NFC unlock feature. + */ + void setNfcUnlockEnabled(int userId, boolean enabled); +} diff --git a/core/java/android/nfc/NdefRecord.java b/core/java/android/nfc/NdefRecord.java index 2b58818..83d17ba 100644 --- a/core/java/android/nfc/NdefRecord.java +++ b/core/java/android/nfc/NdefRecord.java @@ -20,6 +20,7 @@ import android.content.Intent; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; + import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -269,6 +270,7 @@ public final class NdefRecord implements Parcelable { "urn:epc:pat:", // 0x20 "urn:epc:raw:", // 0x21 "urn:epc:", // 0x22 + "urn:nfc:", // 0x23 }; private static final int MAX_PAYLOAD_SIZE = 10 * (1 << 20); // 10 MB payload limit @@ -473,6 +475,45 @@ public final class NdefRecord implements Parcelable { } /** + * Create a new NDEF record containing UTF-8 text data.<p> + * + * The caller can either specify the language code for the provided text, + * or otherwise the language code corresponding to the current default + * locale will be used. + * + * Reference specification: NFCForum-TS-RTD_Text_1.0 + * @param languageCode The languageCode for the record. If locale is empty or null, + * the language code of the current default locale will be used. + * @param text The text to be encoded in the record. Will be represented in UTF-8 format. + * @throws IllegalArgumentException if text is null + */ + public static NdefRecord createTextRecord(String languageCode, String text) { + if (text == null) throw new NullPointerException("text is null"); + + byte[] textBytes = text.getBytes(StandardCharsets.UTF_8); + + byte[] languageCodeBytes = null; + if (languageCode != null && !languageCode.isEmpty()) { + languageCodeBytes = languageCode.getBytes(StandardCharsets.US_ASCII); + } else { + languageCodeBytes = Locale.getDefault().getLanguage(). + getBytes(StandardCharsets.US_ASCII); + } + // We only have 6 bits to indicate ISO/IANA language code. + if (languageCodeBytes.length >= 64) { + throw new IllegalArgumentException("language code is too long, must be <64 bytes."); + } + ByteBuffer buffer = ByteBuffer.allocate(1 + languageCodeBytes.length + textBytes.length); + + byte status = (byte) (languageCodeBytes.length & 0xFF); + buffer.put(status); + buffer.put(languageCodeBytes); + buffer.put(textBytes); + + return new NdefRecord(TNF_WELL_KNOWN, RTD_TEXT, null, buffer.array()); + } + + /** * Construct an NDEF Record from its component fields.<p> * Recommend to use helpers such as {#createUri} or * {{@link #createExternal} where possible, since they perform @@ -774,7 +815,7 @@ public final class NdefRecord implements Parcelable { throw new FormatException("expected TNF_UNCHANGED in non-leading chunk"); } else if (!inChunk && tnf == NdefRecord.TNF_UNCHANGED) { throw new FormatException("" + - "unexpected TNF_UNCHANGED in first chunk or unchunked record"); + "unexpected TNF_UNCHANGED in first chunk or unchunked record"); } int typeLength = buffer.get() & 0xFF; diff --git a/core/java/android/nfc/NfcActivityManager.java b/core/java/android/nfc/NfcActivityManager.java index 77c0234..8643f2e 100644 --- a/core/java/android/nfc/NfcActivityManager.java +++ b/core/java/android/nfc/NfcActivityManager.java @@ -18,6 +18,7 @@ package android.nfc; import android.app.Activity; import android.app.Application; +import android.content.Intent; import android.net.Uri; import android.nfc.NfcAdapter.ReaderCallback; import android.os.Binder; @@ -327,6 +328,7 @@ public final class NfcActivityManager extends IAppCallback.Stub NfcAdapter.CreateNdefMessageCallback ndefCallback; NfcAdapter.CreateBeamUrisCallback urisCallback; NdefMessage message; + Activity activity; Uri[] uris; int flags; synchronized (NfcActivityManager.this) { @@ -338,6 +340,7 @@ public final class NfcActivityManager extends IAppCallback.Stub message = state.ndefMessage; uris = state.uris; flags = state.flags; + activity = state.activity; } // Make callbacks without lock @@ -362,7 +365,13 @@ public final class NfcActivityManager extends IAppCallback.Stub } } } - + if (uris != null && uris.length > 0) { + for (Uri uri : uris) { + // Grant the NFC process permission to read these URIs + activity.grantUriPermission("com.android.nfc", uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + } return new BeamShareData(message, uris, flags); } diff --git a/core/java/android/nfc/NfcAdapter.java b/core/java/android/nfc/NfcAdapter.java index 6743c6c..96a3947 100644 --- a/core/java/android/nfc/NfcAdapter.java +++ b/core/java/android/nfc/NfcAdapter.java @@ -292,6 +292,7 @@ public final class NfcAdapter { static INfcAdapter sService; static INfcTag sTagService; static INfcCardEmulation sCardEmulationService; + static INfcUnlockSettings sNfcUnlockSettingsService; /** * The NfcAdapter object for each application context. @@ -432,6 +433,13 @@ public final class NfcAdapter { throw new UnsupportedOperationException(); } + try { + sNfcUnlockSettingsService = sService.getNfcUnlockSettingsInterface(); + } catch (RemoteException e) { + Log.e(TAG, "could not retrieve NFC unlock settings service"); + sNfcUnlockSettingsService = null; + } + sIsInitialized = true; } if (context == null) { @@ -549,6 +557,22 @@ public final class NfcAdapter { } /** + * Returns the binder interface to the NFC unlock service. + * + * @throws UnsupportedOperationException if the service is not available. + * @hide + */ + public INfcUnlockSettings getNfcUnlockSettingsService() throws UnsupportedOperationException { + isEnabled(); + + if (sNfcUnlockSettingsService == null) { + throw new UnsupportedOperationException("NfcUnlockSettingsService not available"); + } + + return sNfcUnlockSettingsService; + } + + /** * NFC service dead - attempt best effort recovery * @hide */ @@ -1225,6 +1249,45 @@ public final class NfcAdapter { } /** + * Manually invoke Android Beam to share data. + * + * <p>The Android Beam animation is normally only shown when two NFC-capable + * devices come into range. + * By calling this method, an Activity can invoke the Beam animation directly + * even if no other NFC device is in range yet. The Beam animation will then + * prompt the user to tap another NFC-capable device to complete the data + * transfer. + * + * <p>The main advantage of using this method is that it avoids the need for the + * user to tap the screen to complete the transfer, as this method already + * establishes the direction of the transfer and the consent of the user to + * share data. Callers are responsible for making sure that the user has + * consented to sharing data on NFC tap. + * + * <p>Note that to use this method, the passed in Activity must have already + * set data to share over Beam by using method calls such as + * {@link #setNdefPushMessageCallback} or + * {@link #setBeamPushUrisCallback}. + * + * @param activity the current foreground Activity that has registered data to share + * @return whether the Beam animation was successfully invoked + */ + public boolean invokeBeam(Activity activity) { + if (activity == null) { + throw new NullPointerException("activity may not be null."); + } + enforceResumed(activity); + try { + sService.invokeBeam(); + return true; + } catch (RemoteException e) { + Log.e(TAG, "invokeBeam: NFC process has died."); + attemptDeadServiceRecovery(e); + return false; + } + } + + /** * Enable NDEF message push over NFC while this Activity is in the foreground. * * <p>You must explicitly call this method every time the activity is diff --git a/core/java/android/nfc/NfcUnlock.java b/core/java/android/nfc/NfcUnlock.java new file mode 100644 index 0000000..82dcd96 --- /dev/null +++ b/core/java/android/nfc/NfcUnlock.java @@ -0,0 +1,255 @@ +/* + * 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; + +import static com.android.internal.util.Preconditions.checkNotNull; + +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.content.Context; +import android.os.RemoteException; +import android.os.SystemProperties; +import android.util.Log; + +import java.util.HashMap; + +/** + * Provides an interface to read and update NFC unlock settings. + * <p/> + * Allows system services (currently exclusively LockSettingsService) to + * register NFC tags to be used to unlock the device, as well as the ability + * to enable/disable the service entirely. + * + */ +public class NfcUnlock { + + /** + * Action to unlock the device. + * + * @hide + */ + public static final String ACTION_NFC_UNLOCK = "android.nfc.ACTION_NFC_UNLOCK"; + /** + * Permission to unlock the device. + * + * @hide + */ + public static final String NFC_UNLOCK_PERMISSION = "android.permission.NFC_UNLOCK"; + + /** + * Property to enable NFC Unlock + * + * @hide + */ + public static final String PROPERTY = "ro.com.android.nfc.unlock"; + + private static final String TAG = "NfcUnlock"; + private static HashMap<Context, NfcUnlock> sNfcUnlocks = new HashMap<Context, NfcUnlock>(); + + private final Context mContext; + private final boolean mEnabled; + private INfcUnlockSettings sService; + + private NfcUnlock(Context context, INfcUnlockSettings service) { + this.mContext = checkNotNull(context); + this.sService = checkNotNull(service); + this.mEnabled = getPropertyEnabled(); + } + + /** + * Returns an instance of {@link NfcUnlock}. + */ + public static synchronized NfcUnlock getInstance(NfcAdapter nfcAdapter) { + Context context = nfcAdapter.getContext(); + if (context == null) { + Log.e(TAG, "NfcAdapter context is null"); + throw new UnsupportedOperationException(); + } + + NfcUnlock manager = sNfcUnlocks.get(context); + if (manager == null) { + INfcUnlockSettings service = nfcAdapter.getNfcUnlockSettingsService(); + manager = new NfcUnlock(context, service); + sNfcUnlocks.put(context, manager); + } + + return manager; + } + + /** + * Registers the given {@code tag} as an unlock tag. + * + * @return true if the tag was successfully registered. + * @hide + */ + public boolean registerTag(Tag tag) { + enforcePropertyEnabled(); + + int currentUser = ActivityManager.getCurrentUser(); + + try { + return sService.registerTag(currentUser, tag); + } catch (RemoteException e) { + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover NfcUnlockSettingsService"); + return false; + } + + try { + return sService.registerTag(currentUser, tag); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach NfcUnlockSettingsService", ee); + return false; + } + } + } + + /** + * Deregisters the given {@code tag} as an unlock tag. + * + * @return true if the tag was successfully deregistered. + * @hide + */ + public boolean deregisterTag(long timestamp) { + enforcePropertyEnabled(); + int currentUser = ActivityManager.getCurrentUser(); + + try { + return sService.deregisterTag(currentUser, timestamp); + } catch (RemoteException e) { + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover NfcUnlockSettingsService"); + return false; + } + + try { + return sService.deregisterTag(currentUser, timestamp); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach NfcUnlockSettingsService", ee); + return false; + } + } + } + + /** + * Determines the enable state of the NFC unlock feature. + * + * @return true if NFC unlock is enabled. + */ + public boolean getNfcUnlockEnabled() { + enforcePropertyEnabled(); + int currentUser = ActivityManager.getCurrentUser(); + + try { + return sService.getNfcUnlockEnabled(currentUser); + } catch (RemoteException e) { + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover NfcUnlockSettingsService"); + return false; + } + + try { + return sService.getNfcUnlockEnabled(currentUser); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach NfcUnlockSettingsService", ee); + return false; + } + } + } + + /** + * Set the enable state of the NFC unlock feature. + * + * @return true if the setting was successfully persisted. + * @hide + */ + public boolean setNfcUnlockEnabled(boolean enabled) { + enforcePropertyEnabled(); + int currentUser = ActivityManager.getCurrentUser(); + + try { + sService.setNfcUnlockEnabled(currentUser, enabled); + return true; + } catch (RemoteException e) { + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover NfcUnlockSettingsService"); + return false; + } + + try { + sService.setNfcUnlockEnabled(currentUser, enabled); + return true; + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach NfcUnlockSettingsService", ee); + return false; + } + + } + } + + /** + * Returns a list of times (in millis since epoch) corresponding to when + * unlock tags were registered. + * + * @hide + */ + @Nullable + public long[] getTagRegistryTimes() { + enforcePropertyEnabled(); + int currentUser = ActivityManager.getCurrentUser(); + + try { + return sService.getTagRegistryTimes(currentUser); + } catch (RemoteException e) { + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover NfcUnlockSettingsService"); + return null; + } + + try { + return sService.getTagRegistryTimes(currentUser); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach NfcUnlockSettingsService", ee); + return null; + } + } + } + + /** + * @hide + */ + public static boolean getPropertyEnabled() { + return SystemProperties.get(PROPERTY).equals("ON"); + } + + private void recoverService() { + NfcAdapter adapter = NfcAdapter.getDefaultAdapter(mContext); + sService = adapter.getNfcUnlockSettingsService(); + } + + + private void enforcePropertyEnabled() { + if (!mEnabled) { + throw new UnsupportedOperationException("NFC Unlock property is not enabled"); + } + } +} diff --git a/core/java/android/nfc/Tag.java b/core/java/android/nfc/Tag.java index f2cd232..0d261d1 100644 --- a/core/java/android/nfc/Tag.java +++ b/core/java/android/nfc/Tag.java @@ -204,6 +204,14 @@ public final class Tag implements Parcelable { } /** + * For use by NfcService only. + * @hide + */ + public int[] getTechCodeList() { + return mTechList; + } + + /** * Get the Tag Identifier (if it has one). * <p>The tag identifier is a low level serial number, used for anti-collision * and identification. diff --git a/core/java/android/nfc/tech/Ndef.java b/core/java/android/nfc/tech/Ndef.java index f16dc3b..5852ce4 100644 --- a/core/java/android/nfc/tech/Ndef.java +++ b/core/java/android/nfc/tech/Ndef.java @@ -20,7 +20,6 @@ import android.nfc.ErrorCodes; import android.nfc.FormatException; import android.nfc.INfcTag; import android.nfc.NdefMessage; -import android.nfc.NfcAdapter; import android.nfc.Tag; import android.nfc.TagLostException; import android.os.Bundle; diff --git a/core/java/android/nfc/tech/NdefFormatable.java b/core/java/android/nfc/tech/NdefFormatable.java index ffa6a2b..4175cd0 100644 --- a/core/java/android/nfc/tech/NdefFormatable.java +++ b/core/java/android/nfc/tech/NdefFormatable.java @@ -20,7 +20,6 @@ import android.nfc.ErrorCodes; import android.nfc.FormatException; import android.nfc.INfcTag; import android.nfc.NdefMessage; -import android.nfc.NfcAdapter; import android.nfc.Tag; import android.nfc.TagLostException; import android.os.RemoteException; diff --git a/core/java/android/os/AsyncTask.java b/core/java/android/os/AsyncTask.java index 26e09b6..4f91d19 100644 --- a/core/java/android/os/AsyncTask.java +++ b/core/java/android/os/AsyncTask.java @@ -40,7 +40,7 @@ import java.util.concurrent.atomic.AtomicInteger; * and does not constitute a generic threading framework. AsyncTasks should ideally be * used for short operations (a few seconds at the most.) If you need to keep threads * running for long periods of time, it is highly recommended you use the various APIs - * provided by the <code>java.util.concurrent</code> pacakge such as {@link Executor}, + * provided by the <code>java.util.concurrent</code> package such as {@link Executor}, * {@link ThreadPoolExecutor} and {@link FutureTask}.</p> * * <p>An asynchronous task is defined by a computation that runs on a background thread and diff --git a/core/java/android/os/BatteryManager.java b/core/java/android/os/BatteryManager.java index 2e38960..f339e52 100644 --- a/core/java/android/os/BatteryManager.java +++ b/core/java/android/os/BatteryManager.java @@ -16,9 +16,15 @@ package android.os; +import android.os.BatteryProperty; +import android.os.IBatteryPropertiesRegistrar; +import android.os.RemoteException; +import android.os.ServiceManager; + /** * The BatteryManager class contains strings and constants used for values - * in the {@link android.content.Intent#ACTION_BATTERY_CHANGED} Intent. + * in the {@link android.content.Intent#ACTION_BATTERY_CHANGED} Intent, and + * provides a method for querying battery and charging properties. */ public class BatteryManager { /** @@ -121,4 +127,30 @@ public class BatteryManager { /** @hide */ public static final int BATTERY_PLUGGED_ANY = BATTERY_PLUGGED_AC | BATTERY_PLUGGED_USB | BATTERY_PLUGGED_WIRELESS; + + private IBatteryPropertiesRegistrar mBatteryPropertiesRegistrar; + + /** + * Return the requested battery property. + * + * @param id identifier from {@link BatteryProperty} of the requested property + * @return a {@link BatteryProperty} object that returns the property value, or null on error + */ + public BatteryProperty getProperty(int id) throws RemoteException { + if (mBatteryPropertiesRegistrar == null) { + IBinder b = ServiceManager.getService("batteryproperties"); + mBatteryPropertiesRegistrar = + IBatteryPropertiesRegistrar.Stub.asInterface(b); + + if (mBatteryPropertiesRegistrar == null) + return null; + } + + BatteryProperty prop = new BatteryProperty(Integer.MIN_VALUE); + if ((mBatteryPropertiesRegistrar.getProperty(id, prop) == 0) && + (prop.getInt() != Integer.MIN_VALUE)) + return prop; + else + return null; + } } diff --git a/core/java/android/os/BatteryProperties.java b/core/java/android/os/BatteryProperties.java index 5df5214..8f5cf8b 100644 --- a/core/java/android/os/BatteryProperties.java +++ b/core/java/android/os/BatteryProperties.java @@ -15,9 +15,6 @@ package android.os; -import android.os.Parcel; -import android.os.Parcelable; - /** * {@hide} */ @@ -30,11 +27,25 @@ public class BatteryProperties implements Parcelable { public boolean batteryPresent; public int batteryLevel; public int batteryVoltage; - public int batteryCurrentNow; - public int batteryChargeCounter; public int batteryTemperature; public String batteryTechnology; + public BatteryProperties() { + } + + public void set(BatteryProperties other) { + chargerAcOnline = other.chargerAcOnline; + chargerUsbOnline = other.chargerUsbOnline; + chargerWirelessOnline = other.chargerWirelessOnline; + batteryStatus = other.batteryStatus; + batteryHealth = other.batteryHealth; + batteryPresent = other.batteryPresent; + batteryLevel = other.batteryLevel; + batteryVoltage = other.batteryVoltage; + batteryTemperature = other.batteryTemperature; + batteryTechnology = other.batteryTechnology; + } + /* * Parcel read/write code must be kept in sync with * frameworks/native/services/batteryservice/BatteryProperties.cpp @@ -49,8 +60,6 @@ public class BatteryProperties implements Parcelable { batteryPresent = p.readInt() == 1 ? true : false; batteryLevel = p.readInt(); batteryVoltage = p.readInt(); - batteryCurrentNow = p.readInt(); - batteryChargeCounter = p.readInt(); batteryTemperature = p.readInt(); batteryTechnology = p.readString(); } @@ -64,8 +73,6 @@ public class BatteryProperties implements Parcelable { p.writeInt(batteryPresent ? 1 : 0); p.writeInt(batteryLevel); p.writeInt(batteryVoltage); - p.writeInt(batteryCurrentNow); - p.writeInt(batteryChargeCounter); p.writeInt(batteryTemperature); p.writeString(batteryTechnology); } diff --git a/core/java/android/os/BatteryProperty.aidl b/core/java/android/os/BatteryProperty.aidl new file mode 100644 index 0000000..b3f2ec3 --- /dev/null +++ b/core/java/android/os/BatteryProperty.aidl @@ -0,0 +1,19 @@ +/* +** Copyright 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.os; + +parcelable BatteryProperty; diff --git a/core/java/android/os/BatteryProperty.java b/core/java/android/os/BatteryProperty.java new file mode 100644 index 0000000..ec73952 --- /dev/null +++ b/core/java/android/os/BatteryProperty.java @@ -0,0 +1,116 @@ +/* Copyright 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.os; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Battery properties that may be queried using + * {@link BatteryManager#getProperty + * BatteryManager.getProperty()} + */ +public class BatteryProperty implements Parcelable { + /* + * Battery property identifiers. These must match the values in + * frameworks/native/include/batteryservice/BatteryService.h + */ + /** Battery capacity in microampere-hours, as an integer. */ + public static final int CHARGE_COUNTER = 1; + + /** + * Instantaneous battery current in microamperes, as an integer. Positive + * values indicate net current entering the battery from a charge source, + * negative values indicate net current discharging from the battery. + */ + public static final int CURRENT_NOW = 2; + + /** + * Average battery current in microamperes, as an integer. Positive + * values indicate net current entering the battery from a charge source, + * negative values indicate net current discharging from the battery. + * The time period over which the average is computed may depend on the + * fuel gauge hardware and its configuration. + */ + public static final int CURRENT_AVERAGE = 3; + + /** + * Remaining battery capacity as an integer percentage of total capacity + * (with no fractional part). + */ + public static final int CAPACITY = 4; + + private int mValueInt; + + /** + * @hide + */ + public BatteryProperty(int value) { + mValueInt = value; + } + + /** + * @hide + */ + public BatteryProperty() { + mValueInt = Integer.MIN_VALUE; + } + + /** + * Return the value of a property of integer type previously queried + * via {@link BatteryManager#getProperty + * BatteryManager.getProperty()}. If the platform does + * not provide the property queried, this value will be + * Integer.MIN_VALUE. + * + * @return The queried property value, or Integer.MIN_VALUE if not supported. + */ + public int getInt() { + return mValueInt; + } + + /* + * Parcel read/write code must be kept in sync with + * frameworks/native/services/batteryservice/BatteryProperty.cpp + */ + + private BatteryProperty(Parcel p) { + readFromParcel(p); + } + + public void readFromParcel(Parcel p) { + mValueInt = p.readInt(); + } + + public void writeToParcel(Parcel p, int flags) { + p.writeInt(mValueInt); + } + + public static final Parcelable.Creator<BatteryProperty> CREATOR + = new Parcelable.Creator<BatteryProperty>() { + public BatteryProperty createFromParcel(Parcel p) { + return new BatteryProperty(p); + } + + public BatteryProperty[] newArray(int size) { + return new BatteryProperty[size]; + } + }; + + public int describeContents() { + return 0; + } +} diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java index b1a9ea3..426f21e 100644 --- a/core/java/android/os/BatteryStats.java +++ b/core/java/android/os/BatteryStats.java @@ -24,13 +24,15 @@ import java.util.Formatter; import java.util.List; import java.util.Map; +import android.content.Context; import android.content.pm.ApplicationInfo; import android.telephony.SignalStrength; -import android.util.Log; +import android.text.format.DateFormat; import android.util.Printer; -import android.util.Slog; import android.util.SparseArray; import android.util.TimeUtils; +import com.android.internal.os.BatterySipper; +import com.android.internal.os.BatteryStatsHelper; /** * A class providing access to battery usage statistics, including information on @@ -116,19 +118,14 @@ public abstract class BatteryStats implements Parcelable { public static final int STATS_SINCE_CHARGED = 0; /** - * Include only the last run in the stats. - */ - public static final int STATS_LAST = 1; - - /** * Include only the current run in the stats. */ - public static final int STATS_CURRENT = 2; + public static final int STATS_CURRENT = 1; /** * Include only the run since the last time the device was unplugged in the stats. */ - public static final int STATS_SINCE_UNPLUGGED = 3; + public static final int STATS_SINCE_UNPLUGGED = 2; // NOTE: Update this list if you add/change any stats above. // These characters are supposed to represent "total", "last", "current", @@ -153,6 +150,7 @@ public abstract class BatteryStats implements Parcelable { private static final String FOREGROUND_DATA = "fg"; private static final String WAKELOCK_DATA = "wl"; private static final String KERNEL_WAKELOCK_DATA = "kwl"; + private static final String WAKEUP_REASON_DATA = "wr"; private static final String NETWORK_DATA = "nt"; private static final String USER_ACTIVITY_DATA = "ua"; private static final String BATTERY_DATA = "bt"; @@ -160,6 +158,8 @@ public abstract class BatteryStats implements Parcelable { private static final String BATTERY_LEVEL_DATA = "lv"; private static final String WIFI_DATA = "wfl"; private static final String MISC_DATA = "m"; + private static final String GLOBAL_NETWORK_DATA = "gn"; + private static final String HISTORY_STRING_POOL = "hsp"; private static final String HISTORY_DATA = "h"; private static final String SCREEN_BRIGHTNESS_DATA = "br"; private static final String SIGNAL_STRENGTH_TIME_DATA = "sgt"; @@ -167,6 +167,12 @@ public abstract class BatteryStats implements Parcelable { private static final String SIGNAL_STRENGTH_COUNT_DATA = "sgc"; private static final String DATA_CONNECTION_TIME_DATA = "dct"; private static final String DATA_CONNECTION_COUNT_DATA = "dcc"; + private static final String WIFI_STATE_TIME_DATA = "wst"; + private static final String WIFI_STATE_COUNT_DATA = "wsc"; + private static final String BLUETOOTH_STATE_TIME_DATA = "bst"; + private static final String BLUETOOTH_STATE_COUNT_DATA = "bsc"; + private static final String POWER_USE_SUMMARY_DATA = "pws"; + private static final String POWER_USE_ITEM_DATA = "pwi"; private final StringBuilder mFormatBuilder = new StringBuilder(32); private final Formatter mFormatter = new Formatter(mFormatBuilder); @@ -191,6 +197,25 @@ public abstract class BatteryStats implements Parcelable { } /** + * State for keeping track of long counting information. + */ + public static abstract class LongCounter { + + /** + * Returns the count associated with this Counter for the + * selected type of statistics. + * + * @param which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT + */ + public abstract long getCountLocked(int which); + + /** + * Temporary for debugging. + */ + public abstract void logState(Printer pw, String prefix); + } + + /** * State for keeping track of timing information. */ public static abstract class Timer { @@ -207,11 +232,11 @@ public abstract class BatteryStats implements Parcelable { * Returns the total time in microseconds associated with this Timer for the * selected type of statistics. * - * @param batteryRealtime system realtime on battery in microseconds + * @param elapsedRealtimeUs current elapsed realtime of system in microseconds * @param which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT * @return a time in microseconds */ - public abstract long getTotalTimeLocked(long batteryRealtime, int which); + public abstract long getTotalTimeLocked(long elapsedRealtimeUs, int which); /** * Temporary for debugging. @@ -269,30 +294,29 @@ public abstract class BatteryStats implements Parcelable { */ public abstract int getUid(); - public abstract void noteWifiRunningLocked(); - public abstract void noteWifiStoppedLocked(); - public abstract void noteFullWifiLockAcquiredLocked(); - public abstract void noteFullWifiLockReleasedLocked(); - public abstract void noteWifiScanStartedLocked(); - public abstract void noteWifiScanStoppedLocked(); - public abstract void noteWifiBatchedScanStartedLocked(int csph); - public abstract void noteWifiBatchedScanStoppedLocked(); - public abstract void noteWifiMulticastEnabledLocked(); - public abstract void noteWifiMulticastDisabledLocked(); - public abstract void noteAudioTurnedOnLocked(); - public abstract void noteAudioTurnedOffLocked(); - public abstract void noteVideoTurnedOnLocked(); - public abstract void noteVideoTurnedOffLocked(); - public abstract void noteActivityResumedLocked(); - public abstract void noteActivityPausedLocked(); - public abstract long getWifiRunningTime(long batteryRealtime, int which); - public abstract long getFullWifiLockTime(long batteryRealtime, int which); - public abstract long getWifiScanTime(long batteryRealtime, int which); - public abstract long getWifiBatchedScanTime(int csphBin, long batteryRealtime, int which); - public abstract long getWifiMulticastTime(long batteryRealtime, - int which); - public abstract long getAudioTurnedOnTime(long batteryRealtime, int which); - public abstract long getVideoTurnedOnTime(long batteryRealtime, int which); + public abstract void noteWifiRunningLocked(long elapsedRealtime); + public abstract void noteWifiStoppedLocked(long elapsedRealtime); + public abstract void noteFullWifiLockAcquiredLocked(long elapsedRealtime); + public abstract void noteFullWifiLockReleasedLocked(long elapsedRealtime); + public abstract void noteWifiScanStartedLocked(long elapsedRealtime); + public abstract void noteWifiScanStoppedLocked(long elapsedRealtime); + public abstract void noteWifiBatchedScanStartedLocked(int csph, long elapsedRealtime); + public abstract void noteWifiBatchedScanStoppedLocked(long elapsedRealtime); + public abstract void noteWifiMulticastEnabledLocked(long elapsedRealtime); + public abstract void noteWifiMulticastDisabledLocked(long elapsedRealtime); + public abstract void noteAudioTurnedOnLocked(long elapsedRealtime); + public abstract void noteAudioTurnedOffLocked(long elapsedRealtime); + public abstract void noteVideoTurnedOnLocked(long elapsedRealtime); + public abstract void noteVideoTurnedOffLocked(long elapsedRealtime); + public abstract void noteActivityResumedLocked(long elapsedRealtime); + public abstract void noteActivityPausedLocked(long elapsedRealtime); + public abstract long getWifiRunningTime(long elapsedRealtimeUs, int which); + public abstract long getFullWifiLockTime(long elapsedRealtimeUs, int which); + public abstract long getWifiScanTime(long elapsedRealtimeUs, int which); + public abstract long getWifiBatchedScanTime(int csphBin, long elapsedRealtimeUs, int which); + public abstract long getWifiMulticastTime(long elapsedRealtimeUs, int which); + public abstract long getAudioTurnedOnTime(long elapsedRealtimeUs, int which); + public abstract long getVideoTurnedOnTime(long elapsedRealtimeUs, int which); public abstract Timer getForegroundActivityTimer(); public abstract Timer getVibratorOnTimer(); @@ -314,7 +338,10 @@ public abstract class BatteryStats implements Parcelable { public abstract int getUserActivityCount(int type, int which); public abstract boolean hasNetworkActivity(); - public abstract long getNetworkActivityCount(int type, int which); + public abstract long getNetworkActivityBytes(int type, int which); + public abstract long getNetworkActivityPackets(int type, int which); + public abstract long getMobileRadioActiveTime(int which); + public abstract int getMobileRadioActiveCount(int which); public static abstract class Sensor { /* @@ -331,8 +358,9 @@ public abstract class BatteryStats implements Parcelable { } public class Pid { - public long mWakeSum; - public long mWakeStart; + public int mWakeNesting; + public long mWakeSumMs; + public long mWakeStartMs; } /** @@ -350,6 +378,11 @@ public abstract class BatteryStats implements Parcelable { } /** + * Returns true if this process is still active in the battery stats. + */ + public abstract boolean isActive(); + + /** * Returns the total time (in 1/100 sec) spent executing in user code. * * @param which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT. @@ -440,21 +473,76 @@ public abstract class BatteryStats implements Parcelable { } } + public final static class HistoryTag { + public String string; + public int uid; + + public int poolIdx; + + public void setTo(HistoryTag o) { + string = o.string; + uid = o.uid; + poolIdx = o.poolIdx; + } + + public void setTo(String _string, int _uid) { + string = _string; + uid = _uid; + poolIdx = -1; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(string); + dest.writeInt(uid); + } + + public void readFromParcel(Parcel src) { + string = src.readString(); + uid = src.readInt(); + poolIdx = -1; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + HistoryTag that = (HistoryTag) o; + + if (uid != that.uid) return false; + if (!string.equals(that.string)) return false; + + return true; + } + + @Override + public int hashCode() { + int result = string.hashCode(); + result = 31 * result + uid; + return result; + } + } + public final static class HistoryItem implements Parcelable { - static final String TAG = "HistoryItem"; - static final boolean DEBUG = false; - public HistoryItem next; public long time; - - public static final byte CMD_NULL = 0; - public static final byte CMD_UPDATE = 1; - public static final byte CMD_START = 2; - public static final byte CMD_OVERFLOW = 3; - + + public static final byte CMD_UPDATE = 0; // These can be written as deltas + public static final byte CMD_NULL = -1; + public static final byte CMD_START = 4; + public static final byte CMD_CURRENT_TIME = 5; + public static final byte CMD_OVERFLOW = 6; + public byte cmd = CMD_NULL; + /** + * Return whether the command code is a delta data update. + */ + public boolean isDeltaData() { + return cmd == CMD_UPDATE; + } + public byte batteryLevel; public byte batteryStatus; public byte batteryHealth; @@ -464,50 +552,101 @@ public abstract class BatteryStats implements Parcelable { public char batteryVoltage; // Constants from SCREEN_BRIGHTNESS_* - public static final int STATE_BRIGHTNESS_MASK = 0x0000000f; public static final int STATE_BRIGHTNESS_SHIFT = 0; + public static final int STATE_BRIGHTNESS_MASK = 0x7; // Constants from SIGNAL_STRENGTH_* - public static final int STATE_SIGNAL_STRENGTH_MASK = 0x000000f0; - public static final int STATE_SIGNAL_STRENGTH_SHIFT = 4; + public static final int STATE_SIGNAL_STRENGTH_SHIFT = 3; + public static final int STATE_SIGNAL_STRENGTH_MASK = 0x7 << STATE_SIGNAL_STRENGTH_SHIFT; // Constants from ServiceState.STATE_* - public static final int STATE_PHONE_STATE_MASK = 0x00000f00; - public static final int STATE_PHONE_STATE_SHIFT = 8; + public static final int STATE_PHONE_STATE_SHIFT = 6; + public static final int STATE_PHONE_STATE_MASK = 0x7 << STATE_PHONE_STATE_SHIFT; // Constants from DATA_CONNECTION_* - public static final int STATE_DATA_CONNECTION_MASK = 0x0000f000; - public static final int STATE_DATA_CONNECTION_SHIFT = 12; - + public static final int STATE_DATA_CONNECTION_SHIFT = 9; + public static final int STATE_DATA_CONNECTION_MASK = 0x1f << STATE_DATA_CONNECTION_SHIFT; + // These states always appear directly in the first int token // of a delta change; they should be ones that change relatively // frequently. + public static final int STATE_CPU_RUNNING_FLAG = 1<<31; public static final int STATE_WAKE_LOCK_FLAG = 1<<30; - public static final int STATE_SENSOR_ON_FLAG = 1<<29; - public static final int STATE_GPS_ON_FLAG = 1<<28; - public static final int STATE_PHONE_SCANNING_FLAG = 1<<27; - public static final int STATE_WIFI_RUNNING_FLAG = 1<<26; - public static final int STATE_WIFI_FULL_LOCK_FLAG = 1<<25; - public static final int STATE_WIFI_SCAN_FLAG = 1<<24; - public static final int STATE_WIFI_MULTICAST_ON_FLAG = 1<<23; + public static final int STATE_GPS_ON_FLAG = 1<<29; + public static final int STATE_WIFI_FULL_LOCK_FLAG = 1<<28; + public static final int STATE_WIFI_SCAN_FLAG = 1<<27; + public static final int STATE_WIFI_MULTICAST_ON_FLAG = 1<<26; + public static final int STATE_MOBILE_RADIO_ACTIVE_FLAG = 1<<25; + public static final int STATE_WIFI_RUNNING_FLAG = 1<<24; // These are on the lower bits used for the command; if they change // we need to write another int of data. + public static final int STATE_SENSOR_ON_FLAG = 1<<23; public static final int STATE_AUDIO_ON_FLAG = 1<<22; - public static final int STATE_VIDEO_ON_FLAG = 1<<21; + public static final int STATE_PHONE_SCANNING_FLAG = 1<<21; public static final int STATE_SCREEN_ON_FLAG = 1<<20; public static final int STATE_BATTERY_PLUGGED_FLAG = 1<<19; public static final int STATE_PHONE_IN_CALL_FLAG = 1<<18; public static final int STATE_WIFI_ON_FLAG = 1<<17; public static final int STATE_BLUETOOTH_ON_FLAG = 1<<16; - + public static final int MOST_INTERESTING_STATES = STATE_BATTERY_PLUGGED_FLAG | STATE_SCREEN_ON_FLAG | STATE_GPS_ON_FLAG | STATE_PHONE_IN_CALL_FLAG; public int states; + public static final int STATE2_VIDEO_ON_FLAG = 1<<0; + public int states2; + + // The wake lock that was acquired at this point. + public HistoryTag wakelockTag; + + // Kernel wakeup reason at this point. + public HistoryTag wakeReasonTag; + + public static final int EVENT_FLAG_START = 0x8000; + public static final int EVENT_FLAG_FINISH = 0x4000; + + // No event in this item. + public static final int EVENT_NONE = 0x0000; + // Event is about a process that is running. + public static final int EVENT_PROC = 0x0001; + // Event is about an application package that is in the foreground. + public static final int EVENT_FOREGROUND = 0x0002; + // Event is about an application package that is at the top of the screen. + public static final int EVENT_TOP = 0x0003; + // Event is about an application package that is at the top of the screen. + public static final int EVENT_SYNC = 0x0004; + // Number of event types. + public static final int EVENT_COUNT = 0x0005; + + public static final int EVENT_PROC_START = EVENT_PROC | EVENT_FLAG_START; + public static final int EVENT_PROC_FINISH = EVENT_PROC | EVENT_FLAG_FINISH; + public static final int EVENT_FOREGROUND_START = EVENT_FOREGROUND | EVENT_FLAG_START; + public static final int EVENT_FOREGROUND_FINISH = EVENT_FOREGROUND | EVENT_FLAG_FINISH; + public static final int EVENT_TOP_START = EVENT_TOP | EVENT_FLAG_START; + public static final int EVENT_TOP_FINISH = EVENT_TOP | EVENT_FLAG_FINISH; + public static final int EVENT_SYNC_START = EVENT_SYNC | EVENT_FLAG_START; + public static final int EVENT_SYNC_FINISH = EVENT_SYNC | EVENT_FLAG_FINISH; + + // For CMD_EVENT. + public int eventCode; + public HistoryTag eventTag; + + // Only set for CMD_CURRENT_TIME. + public long currentTime; + + // Meta-data when reading. + public int numReadInts; + + // Pre-allocated objects. + public final HistoryTag localWakelockTag = new HistoryTag(); + public final HistoryTag localWakeReasonTag = new HistoryTag(); + public final HistoryTag localEventTag = new HistoryTag(); + public HistoryItem() { } public HistoryItem(long time, Parcel src) { this.time = time; + numReadInts = 2; readFromParcel(src); } @@ -521,168 +660,70 @@ public abstract class BatteryStats implements Parcelable { | ((((int)batteryLevel)<<8)&0xff00) | ((((int)batteryStatus)<<16)&0xf0000) | ((((int)batteryHealth)<<20)&0xf00000) - | ((((int)batteryPlugType)<<24)&0xf000000); + | ((((int)batteryPlugType)<<24)&0xf000000) + | (wakelockTag != null ? 0x10000000 : 0) + | (wakeReasonTag != null ? 0x20000000 : 0) + | (eventCode != EVENT_NONE ? 0x40000000 : 0); dest.writeInt(bat); bat = (((int)batteryTemperature)&0xffff) | ((((int)batteryVoltage)<<16)&0xffff0000); dest.writeInt(bat); dest.writeInt(states); + dest.writeInt(states2); + if (wakelockTag != null) { + wakelockTag.writeToParcel(dest, flags); + } + if (wakeReasonTag != null) { + wakeReasonTag.writeToParcel(dest, flags); + } + if (eventCode != EVENT_NONE) { + dest.writeInt(eventCode); + eventTag.writeToParcel(dest, flags); + } + if (cmd == CMD_CURRENT_TIME) { + dest.writeLong(currentTime); + } } - private void readFromParcel(Parcel src) { + public void readFromParcel(Parcel src) { + int start = src.dataPosition(); int bat = src.readInt(); cmd = (byte)(bat&0xff); batteryLevel = (byte)((bat>>8)&0xff); batteryStatus = (byte)((bat>>16)&0xf); batteryHealth = (byte)((bat>>20)&0xf); batteryPlugType = (byte)((bat>>24)&0xf); - bat = src.readInt(); - batteryTemperature = (short)(bat&0xffff); - batteryVoltage = (char)((bat>>16)&0xffff); + int bat2 = src.readInt(); + batteryTemperature = (short)(bat2&0xffff); + batteryVoltage = (char)((bat2>>16)&0xffff); states = src.readInt(); - } - - // Part of initial delta int that specifies the time delta. - static final int DELTA_TIME_MASK = 0x3ffff; - static final int DELTA_TIME_ABS = 0x3fffd; // Following is an entire abs update. - static final int DELTA_TIME_INT = 0x3fffe; // The delta is a following int - static final int DELTA_TIME_LONG = 0x3ffff; // The delta is a following long - // Part of initial delta int holding the command code. - static final int DELTA_CMD_MASK = 0x3; - static final int DELTA_CMD_SHIFT = 18; - // Flag in delta int: a new battery level int follows. - static final int DELTA_BATTERY_LEVEL_FLAG = 1<<20; - // Flag in delta int: a new full state and battery status int follows. - static final int DELTA_STATE_FLAG = 1<<21; - static final int DELTA_STATE_MASK = 0xffc00000; - - public void writeDelta(Parcel dest, HistoryItem last) { - if (last == null || last.cmd != CMD_UPDATE) { - dest.writeInt(DELTA_TIME_ABS); - writeToParcel(dest, 0); - return; - } - - final long deltaTime = time - last.time; - final int lastBatteryLevelInt = last.buildBatteryLevelInt(); - final int lastStateInt = last.buildStateInt(); - - int deltaTimeToken; - if (deltaTime < 0 || deltaTime > Integer.MAX_VALUE) { - deltaTimeToken = DELTA_TIME_LONG; - } else if (deltaTime >= DELTA_TIME_ABS) { - deltaTimeToken = DELTA_TIME_INT; + states2 = src.readInt(); + if ((bat&0x10000000) != 0) { + wakelockTag = localWakelockTag; + wakelockTag.readFromParcel(src); } else { - deltaTimeToken = (int)deltaTime; - } - int firstToken = deltaTimeToken - | (cmd<<DELTA_CMD_SHIFT) - | (states&DELTA_STATE_MASK); - final int batteryLevelInt = buildBatteryLevelInt(); - final boolean batteryLevelIntChanged = batteryLevelInt != lastBatteryLevelInt; - if (batteryLevelIntChanged) { - firstToken |= DELTA_BATTERY_LEVEL_FLAG; - } - final int stateInt = buildStateInt(); - final boolean stateIntChanged = stateInt != lastStateInt; - if (stateIntChanged) { - firstToken |= DELTA_STATE_FLAG; - } - dest.writeInt(firstToken); - if (DEBUG) Slog.i(TAG, "WRITE DELTA: firstToken=0x" + Integer.toHexString(firstToken) - + " deltaTime=" + deltaTime); - - if (deltaTimeToken >= DELTA_TIME_INT) { - if (deltaTimeToken == DELTA_TIME_INT) { - if (DEBUG) Slog.i(TAG, "WRITE DELTA: int deltaTime=" + (int)deltaTime); - dest.writeInt((int)deltaTime); - } else { - if (DEBUG) Slog.i(TAG, "WRITE DELTA: long deltaTime=" + deltaTime); - dest.writeLong(deltaTime); - } - } - if (batteryLevelIntChanged) { - dest.writeInt(batteryLevelInt); - if (DEBUG) Slog.i(TAG, "WRITE DELTA: batteryToken=0x" - + Integer.toHexString(batteryLevelInt) - + " batteryLevel=" + batteryLevel - + " batteryTemp=" + batteryTemperature - + " batteryVolt=" + (int)batteryVoltage); + wakelockTag = null; } - if (stateIntChanged) { - dest.writeInt(stateInt); - if (DEBUG) Slog.i(TAG, "WRITE DELTA: stateToken=0x" - + Integer.toHexString(stateInt) - + " batteryStatus=" + batteryStatus - + " batteryHealth=" + batteryHealth - + " batteryPlugType=" + batteryPlugType - + " states=0x" + Integer.toHexString(states)); - } - } - - private int buildBatteryLevelInt() { - return ((((int)batteryLevel)<<25)&0xfe000000) - | ((((int)batteryTemperature)<<14)&0x01ffc000) - | (((int)batteryVoltage)&0x00003fff); - } - - private int buildStateInt() { - return ((((int)batteryStatus)<<28)&0xf0000000) - | ((((int)batteryHealth)<<24)&0x0f000000) - | ((((int)batteryPlugType)<<22)&0x00c00000) - | (states&(~DELTA_STATE_MASK)); - } - - public void readDelta(Parcel src) { - int firstToken = src.readInt(); - int deltaTimeToken = firstToken&DELTA_TIME_MASK; - cmd = (byte)((firstToken>>DELTA_CMD_SHIFT)&DELTA_CMD_MASK); - if (DEBUG) Slog.i(TAG, "READ DELTA: firstToken=0x" + Integer.toHexString(firstToken) - + " deltaTimeToken=" + deltaTimeToken); - - if (deltaTimeToken < DELTA_TIME_ABS) { - time += deltaTimeToken; - } else if (deltaTimeToken == DELTA_TIME_ABS) { - time = src.readLong(); - readFromParcel(src); - return; - } else if (deltaTimeToken == DELTA_TIME_INT) { - int delta = src.readInt(); - time += delta; - if (DEBUG) Slog.i(TAG, "READ DELTA: time delta=" + delta + " new time=" + time); + if ((bat&0x20000000) != 0) { + wakeReasonTag = localWakeReasonTag; + wakeReasonTag.readFromParcel(src); } else { - long delta = src.readLong(); - if (DEBUG) Slog.i(TAG, "READ DELTA: time delta=" + delta + " new time=" + time); - time += delta; + wakeReasonTag = null; } - - if ((firstToken&DELTA_BATTERY_LEVEL_FLAG) != 0) { - int batteryLevelInt = src.readInt(); - batteryLevel = (byte)((batteryLevelInt>>25)&0x7f); - batteryTemperature = (short)((batteryLevelInt<<7)>>21); - batteryVoltage = (char)(batteryLevelInt&0x3fff); - if (DEBUG) Slog.i(TAG, "READ DELTA: batteryToken=0x" - + Integer.toHexString(batteryLevelInt) - + " batteryLevel=" + batteryLevel - + " batteryTemp=" + batteryTemperature - + " batteryVolt=" + (int)batteryVoltage); + if ((bat&0x40000000) != 0) { + eventCode = src.readInt(); + eventTag = localEventTag; + eventTag.readFromParcel(src); + } else { + eventCode = EVENT_NONE; + eventTag = null; } - - if ((firstToken&DELTA_STATE_FLAG) != 0) { - int stateInt = src.readInt(); - states = (firstToken&DELTA_STATE_MASK) | (stateInt&(~DELTA_STATE_MASK)); - batteryStatus = (byte)((stateInt>>28)&0xf); - batteryHealth = (byte)((stateInt>>24)&0xf); - batteryPlugType = (byte)((stateInt>>22)&0x3); - if (DEBUG) Slog.i(TAG, "READ DELTA: stateToken=0x" - + Integer.toHexString(stateInt) - + " batteryStatus=" + batteryStatus - + " batteryHealth=" + batteryHealth - + " batteryPlugType=" + batteryPlugType - + " states=0x" + Integer.toHexString(states)); + if (cmd == CMD_CURRENT_TIME) { + currentTime = src.readLong(); } else { - states = (firstToken&DELTA_STATE_MASK) | (states&(~DELTA_STATE_MASK)); + currentTime = 0; } + numReadInts += (src.dataPosition()-start)/4; } public void clear() { @@ -695,23 +736,26 @@ public abstract class BatteryStats implements Parcelable { batteryTemperature = 0; batteryVoltage = 0; states = 0; + states2 = 0; + wakelockTag = null; + wakeReasonTag = null; + eventCode = EVENT_NONE; + eventTag = null; } public void setTo(HistoryItem o) { time = o.time; cmd = o.cmd; - batteryLevel = o.batteryLevel; - batteryStatus = o.batteryStatus; - batteryHealth = o.batteryHealth; - batteryPlugType = o.batteryPlugType; - batteryTemperature = o.batteryTemperature; - batteryVoltage = o.batteryVoltage; - states = o.states; + setToCommon(o); } public void setTo(long time, byte cmd, HistoryItem o) { this.time = time; this.cmd = cmd; + setToCommon(o); + } + + private void setToCommon(HistoryItem o) { batteryLevel = o.batteryLevel; batteryStatus = o.batteryStatus; batteryHealth = o.batteryHealth; @@ -719,16 +763,70 @@ public abstract class BatteryStats implements Parcelable { batteryTemperature = o.batteryTemperature; batteryVoltage = o.batteryVoltage; states = o.states; + states2 = o.states2; + if (o.wakelockTag != null) { + wakelockTag = localWakelockTag; + wakelockTag.setTo(o.wakelockTag); + } else { + wakelockTag = null; + } + if (o.wakeReasonTag != null) { + wakeReasonTag = localWakeReasonTag; + wakeReasonTag.setTo(o.wakeReasonTag); + } else { + wakeReasonTag = null; + } + eventCode = o.eventCode; + if (o.eventTag != null) { + eventTag = localEventTag; + eventTag.setTo(o.eventTag); + } else { + eventTag = null; + } + currentTime = o.currentTime; } - public boolean same(HistoryItem o) { + public boolean sameNonEvent(HistoryItem o) { return batteryLevel == o.batteryLevel && batteryStatus == o.batteryStatus && batteryHealth == o.batteryHealth && batteryPlugType == o.batteryPlugType && batteryTemperature == o.batteryTemperature && batteryVoltage == o.batteryVoltage - && states == o.states; + && states == o.states + && states2 == o.states2 + && currentTime == o.currentTime; + } + + public boolean same(HistoryItem o) { + if (!sameNonEvent(o) || eventCode != o.eventCode) { + return false; + } + if (wakelockTag != o.wakelockTag) { + if (wakelockTag == null || o.wakelockTag == null) { + return false; + } + if (!wakelockTag.equals(o.wakelockTag)) { + return false; + } + } + if (wakeReasonTag != o.wakeReasonTag) { + if (wakeReasonTag == null || o.wakeReasonTag == null) { + return false; + } + if (!wakeReasonTag.equals(o.wakeReasonTag)) { + return false; + } + } + if (eventTag != o.eventTag) { + if (eventTag == null || o.eventTag == null) { + return false; + } + if (!eventTag.equals(o.eventTag)) { + return false; + } + } + return true; } } @@ -736,25 +834,44 @@ public abstract class BatteryStats implements Parcelable { public final int mask; public final int shift; public final String name; + public final String shortName; public final String[] values; + public final String[] shortValues; - public BitDescription(int mask, String name) { + public BitDescription(int mask, String name, String shortName) { this.mask = mask; this.shift = -1; this.name = name; + this.shortName = shortName; this.values = null; + this.shortValues = null; } - public BitDescription(int mask, int shift, String name, String[] values) { + public BitDescription(int mask, int shift, String name, String shortName, + String[] values, String[] shortValues) { this.mask = mask; this.shift = shift; this.name = name; + this.shortName = shortName; this.values = values; + this.shortValues = shortValues; } } + public abstract int getHistoryTotalSize(); + + public abstract int getHistoryUsedSize(); + public abstract boolean startIteratingHistoryLocked(); + public abstract int getHistoryStringPoolSize(); + + public abstract int getHistoryStringPoolBytes(); + + public abstract String getHistoryTagPoolString(int index); + + public abstract int getHistoryTagPoolUid(int index); + public abstract boolean getNextHistoryLocked(HistoryItem out); public abstract void finishIteratingHistoryLocked(); @@ -781,8 +898,15 @@ public abstract class BatteryStats implements Parcelable { * * {@hide} */ - public abstract long getScreenOnTime(long batteryRealtime, int which); + public abstract long getScreenOnTime(long elapsedRealtimeUs, int which); + /** + * Returns the number of times the screen was turned on. + * + * {@hide} + */ + public abstract int getScreenOnCount(int which); + public static final int SCREEN_BRIGHTNESS_DARK = 0; public static final int SCREEN_BRIGHTNESS_DIM = 1; public static final int SCREEN_BRIGHTNESS_MEDIUM = 2; @@ -793,6 +917,10 @@ public abstract class BatteryStats implements Parcelable { "dark", "dim", "medium", "light", "bright" }; + static final String[] SCREEN_BRIGHTNESS_SHORT_NAMES = { + "0", "1", "2", "3", "4" + }; + public static final int NUM_SCREEN_BRIGHTNESS_BINS = 5; /** @@ -802,7 +930,7 @@ public abstract class BatteryStats implements Parcelable { * {@hide} */ public abstract long getScreenBrightnessTime(int brightnessBin, - long batteryRealtime, int which); + long elapsedRealtimeUs, int which); public abstract int getInputEventCount(int which); @@ -812,16 +940,23 @@ public abstract class BatteryStats implements Parcelable { * * {@hide} */ - public abstract long getPhoneOnTime(long batteryRealtime, int which); + public abstract long getPhoneOnTime(long elapsedRealtimeUs, int which); /** + * Returns the number of times a phone call was activated. + * + * {@hide} + */ + public abstract int getPhoneOnCount(int which); + + /** * Returns the time in microseconds that the phone has been running with * the given signal strength. * * {@hide} */ public abstract long getPhoneSignalStrengthTime(int strengthBin, - long batteryRealtime, int which); + long elapsedRealtimeUs, int which); /** * Returns the time in microseconds that the phone has been trying to @@ -830,7 +965,7 @@ public abstract class BatteryStats implements Parcelable { * {@hide} */ public abstract long getPhoneSignalScanningTime( - long batteryRealtime, int which); + long elapsedRealtimeUs, int which); /** * Returns the number of times the phone has entered the given signal strength. @@ -839,6 +974,46 @@ public abstract class BatteryStats implements Parcelable { */ public abstract int getPhoneSignalStrengthCount(int strengthBin, int which); + /** + * Returns the time in microseconds that the mobile network has been active + * (in a high power state). + * + * {@hide} + */ + public abstract long getMobileRadioActiveTime(long elapsedRealtimeUs, int which); + + /** + * Returns the number of times that the mobile network has transitioned to the + * active state. + * + * {@hide} + */ + public abstract int getMobileRadioActiveCount(int which); + + /** + * Returns the time in microseconds that is the difference between the mobile radio + * time we saw based on the elapsed timestamp when going down vs. the given time stamp + * from the radio. + * + * {@hide} + */ + public abstract long getMobileRadioActiveAdjustedTime(int which); + + /** + * Returns the time in microseconds that the mobile network has been active + * (in a high power state) but not being able to blame on an app. + * + * {@hide} + */ + public abstract long getMobileRadioActiveUnknownTime(int which); + + /** + * Return count of number of times radio was up that could not be blamed on apps. + * + * {@hide} + */ + public abstract int getMobileRadioActiveUnknownCount(int which); + public static final int DATA_CONNECTION_NONE = 0; public static final int DATA_CONNECTION_GPRS = 1; public static final int DATA_CONNECTION_EDGE = 2; @@ -872,7 +1047,7 @@ public abstract class BatteryStats implements Parcelable { * {@hide} */ public abstract long getPhoneDataConnectionTime(int dataType, - long batteryRealtime, int which); + long elapsedRealtimeUs, int which); /** * Returns the number of times the phone has entered the given data @@ -881,36 +1056,53 @@ public abstract class BatteryStats implements Parcelable { * {@hide} */ public abstract int getPhoneDataConnectionCount(int dataType, int which); - + public static final BitDescription[] HISTORY_STATE_DESCRIPTIONS = new BitDescription[] { - new BitDescription(HistoryItem.STATE_BATTERY_PLUGGED_FLAG, "plugged"), - new BitDescription(HistoryItem.STATE_SCREEN_ON_FLAG, "screen"), - new BitDescription(HistoryItem.STATE_GPS_ON_FLAG, "gps"), - new BitDescription(HistoryItem.STATE_PHONE_IN_CALL_FLAG, "phone_in_call"), - new BitDescription(HistoryItem.STATE_PHONE_SCANNING_FLAG, "phone_scanning"), - new BitDescription(HistoryItem.STATE_WIFI_ON_FLAG, "wifi"), - new BitDescription(HistoryItem.STATE_WIFI_RUNNING_FLAG, "wifi_running"), - new BitDescription(HistoryItem.STATE_WIFI_FULL_LOCK_FLAG, "wifi_full_lock"), - new BitDescription(HistoryItem.STATE_WIFI_SCAN_FLAG, "wifi_scan"), - new BitDescription(HistoryItem.STATE_WIFI_MULTICAST_ON_FLAG, "wifi_multicast"), - new BitDescription(HistoryItem.STATE_BLUETOOTH_ON_FLAG, "bluetooth"), - new BitDescription(HistoryItem.STATE_AUDIO_ON_FLAG, "audio"), - new BitDescription(HistoryItem.STATE_VIDEO_ON_FLAG, "video"), - new BitDescription(HistoryItem.STATE_WAKE_LOCK_FLAG, "wake_lock"), - new BitDescription(HistoryItem.STATE_SENSOR_ON_FLAG, "sensor"), - new BitDescription(HistoryItem.STATE_BRIGHTNESS_MASK, - HistoryItem.STATE_BRIGHTNESS_SHIFT, "brightness", - SCREEN_BRIGHTNESS_NAMES), - new BitDescription(HistoryItem.STATE_SIGNAL_STRENGTH_MASK, - HistoryItem.STATE_SIGNAL_STRENGTH_SHIFT, "signal_strength", - SignalStrength.SIGNAL_STRENGTH_NAMES), - new BitDescription(HistoryItem.STATE_PHONE_STATE_MASK, - HistoryItem.STATE_PHONE_STATE_SHIFT, "phone_state", - new String[] {"in", "out", "emergency", "off"}), + new BitDescription(HistoryItem.STATE_CPU_RUNNING_FLAG, "running", "r"), + new BitDescription(HistoryItem.STATE_WAKE_LOCK_FLAG, "wake_lock", "w"), + new BitDescription(HistoryItem.STATE_SENSOR_ON_FLAG, "sensor", "s"), + new BitDescription(HistoryItem.STATE_GPS_ON_FLAG, "gps", "g"), + new BitDescription(HistoryItem.STATE_WIFI_FULL_LOCK_FLAG, "wifi_full_lock", "Wl"), + new BitDescription(HistoryItem.STATE_WIFI_SCAN_FLAG, "wifi_scan", "Ws"), + new BitDescription(HistoryItem.STATE_WIFI_MULTICAST_ON_FLAG, "wifi_multicast", "Wm"), + new BitDescription(HistoryItem.STATE_MOBILE_RADIO_ACTIVE_FLAG, "mobile_radio", "Pr"), + new BitDescription(HistoryItem.STATE_WIFI_RUNNING_FLAG, "wifi_running", "Wr"), + new BitDescription(HistoryItem.STATE_PHONE_SCANNING_FLAG, "phone_scanning", "Psc"), + new BitDescription(HistoryItem.STATE_AUDIO_ON_FLAG, "audio", "a"), + new BitDescription(HistoryItem.STATE_SCREEN_ON_FLAG, "screen", "S"), + new BitDescription(HistoryItem.STATE_BATTERY_PLUGGED_FLAG, "plugged", "BP"), + new BitDescription(HistoryItem.STATE_PHONE_IN_CALL_FLAG, "phone_in_call", "Pcl"), + new BitDescription(HistoryItem.STATE_WIFI_ON_FLAG, "wifi", "W"), + new BitDescription(HistoryItem.STATE_BLUETOOTH_ON_FLAG, "bluetooth", "b"), new BitDescription(HistoryItem.STATE_DATA_CONNECTION_MASK, - HistoryItem.STATE_DATA_CONNECTION_SHIFT, "data_conn", - DATA_CONNECTION_NAMES), + HistoryItem.STATE_DATA_CONNECTION_SHIFT, "data_conn", "Pcn", + DATA_CONNECTION_NAMES, DATA_CONNECTION_NAMES), + new BitDescription(HistoryItem.STATE_PHONE_STATE_MASK, + HistoryItem.STATE_PHONE_STATE_SHIFT, "phone_state", "Pst", + new String[] {"in", "out", "emergency", "off"}, + new String[] {"in", "out", "em", "off"}), + new BitDescription(HistoryItem.STATE_SIGNAL_STRENGTH_MASK, + HistoryItem.STATE_SIGNAL_STRENGTH_SHIFT, "signal_strength", "Pss", + SignalStrength.SIGNAL_STRENGTH_NAMES, new String[] { + "0", "1", "2", "3", "4" + }), + new BitDescription(HistoryItem.STATE_BRIGHTNESS_MASK, + HistoryItem.STATE_BRIGHTNESS_SHIFT, "brightness", "Sb", + SCREEN_BRIGHTNESS_NAMES, SCREEN_BRIGHTNESS_SHORT_NAMES), + }; + + public static final BitDescription[] HISTORY_STATE2_DESCRIPTIONS + = new BitDescription[] { + new BitDescription(HistoryItem.STATE2_VIDEO_ON_FLAG, "video", "v"), + }; + + public static final String[] HISTORY_EVENT_NAMES = new String[] { + "null", "proc", "fg", "top", "sync" + }; + + public static final String[] HISTORY_EVENT_CHECKIN_NAMES = new String[] { + "Enl", "Epr", "Efg", "Etp", "Esy" }; /** @@ -919,7 +1111,7 @@ public abstract class BatteryStats implements Parcelable { * * {@hide} */ - public abstract long getWifiOnTime(long batteryRealtime, int which); + public abstract long getWifiOnTime(long elapsedRealtimeUs, int which); /** * Returns the time in microseconds that wifi has been on and the driver has @@ -927,7 +1119,38 @@ public abstract class BatteryStats implements Parcelable { * * {@hide} */ - public abstract long getGlobalWifiRunningTime(long batteryRealtime, int which); + public abstract long getGlobalWifiRunningTime(long elapsedRealtimeUs, int which); + + public static final int WIFI_STATE_OFF = 0; + public static final int WIFI_STATE_OFF_SCANNING = 1; + public static final int WIFI_STATE_ON_NO_NETWORKS = 2; + public static final int WIFI_STATE_ON_DISCONNECTED = 3; + public static final int WIFI_STATE_ON_CONNECTED_STA = 4; + public static final int WIFI_STATE_ON_CONNECTED_P2P = 5; + public static final int WIFI_STATE_ON_CONNECTED_STA_P2P = 6; + public static final int WIFI_STATE_SOFT_AP = 7; + + static final String[] WIFI_STATE_NAMES = { + "off", "scanning", "no_net", "disconn", + "sta", "p2p", "sta_p2p", "soft_ap" + }; + + public static final int NUM_WIFI_STATES = WIFI_STATE_SOFT_AP+1; + + /** + * Returns the time in microseconds that WiFi has been running in the given state. + * + * {@hide} + */ + public abstract long getWifiStateTime(int wifiState, + long elapsedRealtimeUs, int which); + + /** + * Returns the number of times that WiFi has entered the given state. + * + * {@hide} + */ + public abstract int getWifiStateCount(int wifiState, int which); /** * Returns the time in microseconds that bluetooth has been on while the device was @@ -935,16 +1158,51 @@ public abstract class BatteryStats implements Parcelable { * * {@hide} */ - public abstract long getBluetoothOnTime(long batteryRealtime, int which); + public abstract long getBluetoothOnTime(long elapsedRealtimeUs, int which); - public static final int NETWORK_MOBILE_RX_BYTES = 0; - public static final int NETWORK_MOBILE_TX_BYTES = 1; - public static final int NETWORK_WIFI_RX_BYTES = 2; - public static final int NETWORK_WIFI_TX_BYTES = 3; + public abstract int getBluetoothPingCount(); + + public static final int BLUETOOTH_STATE_INACTIVE = 0; + public static final int BLUETOOTH_STATE_LOW = 1; + public static final int BLUETOOTH_STATE_MEDIUM = 2; + public static final int BLUETOOTH_STATE_HIGH = 3; + + static final String[] BLUETOOTH_STATE_NAMES = { + "inactive", "low", "med", "high" + }; + + public static final int NUM_BLUETOOTH_STATES = BLUETOOTH_STATE_HIGH +1; + + /** + * Returns the time in microseconds that Bluetooth has been running in the + * given active state. + * + * {@hide} + */ + public abstract long getBluetoothStateTime(int bluetoothState, + long elapsedRealtimeUs, int which); - public static final int NUM_NETWORK_ACTIVITY_TYPES = NETWORK_WIFI_TX_BYTES + 1; + /** + * Returns the number of times that Bluetooth has entered the given active state. + * + * {@hide} + */ + public abstract int getBluetoothStateCount(int bluetoothState, int which); + + public static final int NETWORK_MOBILE_RX_DATA = 0; + public static final int NETWORK_MOBILE_TX_DATA = 1; + public static final int NETWORK_WIFI_RX_DATA = 2; + public static final int NETWORK_WIFI_TX_DATA = 3; - public abstract long getNetworkActivityCount(int type, int which); + public static final int NUM_NETWORK_ACTIVITY_TYPES = NETWORK_WIFI_TX_DATA + 1; + + public abstract long getNetworkActivityBytes(int type, int which); + public abstract long getNetworkActivityPackets(int type, int which); + + /** + * Return the wall clock time when battery stats data collection started. + */ + public abstract long getStartClockTime(); /** * Return whether we are currently running on battery. @@ -964,19 +1222,6 @@ public abstract class BatteryStats implements Parcelable { public abstract long getBatteryUptime(long curTime); /** - * @deprecated use getRadioDataUptime - */ - public long getRadioDataUptimeMs() { - return getRadioDataUptime() / 1000; - } - - /** - * Returns the time that the radio was on for data transfers. - * @return the uptime in microseconds while unplugged - */ - public abstract long getRadioDataUptime(); - - /** * Returns the current battery realtime in microseconds. * * @param curTime the amount of elapsed realtime in microseconds. @@ -1008,6 +1253,11 @@ public abstract class BatteryStats implements Parcelable { public abstract int getHighDischargeAmountSinceCharge(); /** + * Retrieve the discharge amount over the selected discharge period <var>which</var>. + */ + public abstract int getDischargeAmount(int which); + + /** * Get the amount the battery has discharged while the screen was on, * since the last time power was unplugged. */ @@ -1048,6 +1298,22 @@ public abstract class BatteryStats implements Parcelable { public abstract long computeBatteryRealtime(long curTime, int which); /** + * Returns the total, last, or current battery screen off uptime in microseconds. + * + * @param curTime the elapsed realtime in microseconds. + * @param which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT. + */ + public abstract long computeBatteryScreenOffUptime(long curTime, int which); + + /** + * Returns the total, last, or current battery screen off realtime in microseconds. + * + * @param curTime the current elapsed realtime in microseconds. + * @param which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT. + */ + public abstract long computeBatteryScreenOffRealtime(long curTime, int which); + + /** * Returns the total, last, or current uptime in microseconds. * * @param curTime the current elapsed realtime in microseconds. @@ -1063,11 +1329,15 @@ public abstract class BatteryStats implements Parcelable { */ public abstract long computeRealtime(long curTime, int which); + public abstract Map<String, ? extends LongCounter> getWakeupReasonStats(); + public abstract Map<String, ? extends Timer> getKernelWakelockStats(); /** Returns the number of different speeds that the CPU can run at */ public abstract int getCpuSpeedSteps(); + public abstract void writeToParcelWithoutUids(Parcel out, int flags); + private final static void formatTimeRaw(StringBuilder out, long seconds) { long days = seconds / (60 * 60 * 24); if (days != 0) { @@ -1096,23 +1366,30 @@ public abstract class BatteryStats implements Parcelable { } } - private final static void formatTime(StringBuilder sb, long time) { + public final static void formatTime(StringBuilder sb, long time) { long sec = time / 100; formatTimeRaw(sb, sec); sb.append((time - (sec * 100)) * 10); sb.append("ms "); } - private final static void formatTimeMs(StringBuilder sb, long time) { + public final static void formatTimeMs(StringBuilder sb, long time) { long sec = time / 1000; formatTimeRaw(sb, sec); sb.append(time - (sec * 1000)); sb.append("ms "); } - private final String formatRatioLocked(long num, long den) { + public final static void formatTimeMsNoSpace(StringBuilder sb, long time) { + long sec = time / 1000; + formatTimeRaw(sb, sec); + sb.append(time - (sec * 1000)); + sb.append("ms"); + } + + public final String formatRatioLocked(long num, long den) { if (den == 0L) { - return "---%"; + return "--%"; } float perc = ((float)num) / ((float)den) * 100; mFormatBuilder.setLength(0); @@ -1120,7 +1397,7 @@ public abstract class BatteryStats implements Parcelable { return mFormatBuilder.toString(); } - private final String formatBytesLocked(long bytes) { + final String formatBytesLocked(long bytes) { mFormatBuilder.setLength(0); if (bytes < BYTES_PER_KB) { @@ -1137,10 +1414,10 @@ public abstract class BatteryStats implements Parcelable { } } - private static long computeWakeLock(Timer timer, long batteryRealtime, int which) { + private static long computeWakeLock(Timer timer, long elapsedRealtimeUs, int which) { if (timer != null) { // Convert from microseconds to milliseconds with rounding - long totalTimeMicros = timer.getTotalTimeLocked(batteryRealtime, which); + long totalTimeMicros = timer.getTotalTimeLocked(elapsedRealtimeUs, which); long totalTimeMillis = (totalTimeMicros + 500) / 1000; return totalTimeMillis; } @@ -1151,17 +1428,17 @@ public abstract class BatteryStats implements Parcelable { * * @param sb a StringBuilder object. * @param timer a Timer object contining the wakelock times. - * @param batteryRealtime the current on-battery time in microseconds. + * @param elapsedRealtimeUs the current on-battery time in microseconds. * @param name the name of the wakelock. * @param which which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT. * @param linePrefix a String to be prepended to each line of output. * @return the line prefix */ private static final String printWakeLock(StringBuilder sb, Timer timer, - long batteryRealtime, String name, int which, String linePrefix) { + long elapsedRealtimeUs, String name, int which, String linePrefix) { if (timer != null) { - long totalTimeMillis = computeWakeLock(timer, batteryRealtime, which); + long totalTimeMillis = computeWakeLock(timer, elapsedRealtimeUs, which); int count = timer.getCountLocked(which); if (totalTimeMillis != 0) { @@ -1185,18 +1462,18 @@ public abstract class BatteryStats implements Parcelable { * * @param sb a StringBuilder object. * @param timer a Timer object contining the wakelock times. - * @param now the current time in microseconds. + * @param elapsedRealtimeUs the current time in microseconds. * @param name the name of the wakelock. * @param which which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT. * @param linePrefix a String to be prepended to each line of output. * @return the line prefix */ - private static final String printWakeLockCheckin(StringBuilder sb, Timer timer, long now, - String name, int which, String linePrefix) { + private static final String printWakeLockCheckin(StringBuilder sb, Timer timer, + long elapsedRealtimeUs, String name, int which, String linePrefix) { long totalTimeMicros = 0; int count = 0; if (timer != null) { - totalTimeMicros = timer.getTotalTimeLocked(now, which); + totalTimeMicros = timer.getTotalTimeLocked(elapsedRealtimeUs, which); count = timer.getCountLocked(which); } sb.append(linePrefix); @@ -1234,20 +1511,22 @@ public abstract class BatteryStats implements Parcelable { * * NOTE: all times are expressed in 'ms'. */ - public final void dumpCheckinLocked(PrintWriter pw, int which, int reqUid) { + public final void dumpCheckinLocked(Context context, PrintWriter pw, int which, int reqUid) { final long rawUptime = SystemClock.uptimeMillis() * 1000; final long rawRealtime = SystemClock.elapsedRealtime() * 1000; final long batteryUptime = getBatteryUptime(rawUptime); - final long batteryRealtime = getBatteryRealtime(rawRealtime); final long whichBatteryUptime = computeBatteryUptime(rawUptime, which); final long whichBatteryRealtime = computeBatteryRealtime(rawRealtime, which); + final long whichBatteryScreenOffUptime = computeBatteryScreenOffUptime(rawUptime, which); + final long whichBatteryScreenOffRealtime = computeBatteryScreenOffRealtime(rawRealtime, + which); final long totalRealtime = computeRealtime(rawRealtime, which); final long totalUptime = computeUptime(rawUptime, which); - final long screenOnTime = getScreenOnTime(batteryRealtime, which); - final long phoneOnTime = getPhoneOnTime(batteryRealtime, which); - final long wifiOnTime = getWifiOnTime(batteryRealtime, which); - final long wifiRunningTime = getGlobalWifiRunningTime(batteryRealtime, which); - final long bluetoothOnTime = getBluetoothOnTime(batteryRealtime, which); + final long screenOnTime = getScreenOnTime(rawRealtime, which); + final long phoneOnTime = getPhoneOnTime(rawRealtime, which); + final long wifiOnTime = getWifiOnTime(rawRealtime, which); + final long wifiRunningTime = getGlobalWifiRunningTime(rawRealtime, which); + final long bluetoothOnTime = getBluetoothOnTime(rawRealtime, which); StringBuilder sb = new StringBuilder(128); @@ -1260,22 +1539,16 @@ public abstract class BatteryStats implements Parcelable { dumpLine(pw, 0 /* uid */, category, BATTERY_DATA, which == STATS_SINCE_CHARGED ? getStartCount() : "N/A", whichBatteryRealtime / 1000, whichBatteryUptime / 1000, - totalRealtime / 1000, totalUptime / 1000); + totalRealtime / 1000, totalUptime / 1000, + getStartClockTime(), + whichBatteryScreenOffRealtime / 1000, whichBatteryScreenOffUptime / 1000); - // Calculate total network and wakelock times across all uids. - long mobileRxTotal = 0; - long mobileTxTotal = 0; - long wifiRxTotal = 0; - long wifiTxTotal = 0; + // Calculate wakelock times across all uids. long fullWakeLockTimeTotal = 0; long partialWakeLockTimeTotal = 0; for (int iu = 0; iu < NU; iu++) { Uid u = uidStats.valueAt(iu); - mobileRxTotal += u.getNetworkActivityCount(NETWORK_MOBILE_RX_BYTES, which); - mobileTxTotal += u.getNetworkActivityCount(NETWORK_MOBILE_TX_BYTES, which); - wifiRxTotal += u.getNetworkActivityCount(NETWORK_WIFI_RX_BYTES, which); - wifiTxTotal += u.getNetworkActivityCount(NETWORK_WIFI_TX_BYTES, which); Map<String, ? extends BatteryStats.Uid.Wakelock> wakelocks = u.getWakelockStats(); if (wakelocks.size() > 0) { @@ -1285,59 +1558,97 @@ public abstract class BatteryStats implements Parcelable { Timer fullWakeTimer = wl.getWakeTime(WAKE_TYPE_FULL); if (fullWakeTimer != null) { - fullWakeLockTimeTotal += fullWakeTimer.getTotalTimeLocked(batteryRealtime, which); + fullWakeLockTimeTotal += fullWakeTimer.getTotalTimeLocked(rawRealtime, + which); } Timer partialWakeTimer = wl.getWakeTime(WAKE_TYPE_PARTIAL); if (partialWakeTimer != null) { partialWakeLockTimeTotal += partialWakeTimer.getTotalTimeLocked( - batteryRealtime, which); + rawRealtime, which); } } } } + long mobileRxTotalBytes = getNetworkActivityBytes(NETWORK_MOBILE_RX_DATA, which); + long mobileTxTotalBytes = getNetworkActivityBytes(NETWORK_MOBILE_TX_DATA, which); + long wifiRxTotalBytes = getNetworkActivityBytes(NETWORK_WIFI_RX_DATA, which); + long wifiTxTotalBytes = getNetworkActivityBytes(NETWORK_WIFI_TX_DATA, which); + long mobileRxTotalPackets = getNetworkActivityPackets(NETWORK_MOBILE_RX_DATA, which); + long mobileTxTotalPackets = getNetworkActivityPackets(NETWORK_MOBILE_TX_DATA, which); + long wifiRxTotalPackets = getNetworkActivityPackets(NETWORK_WIFI_RX_DATA, which); + long wifiTxTotalPackets = getNetworkActivityPackets(NETWORK_WIFI_TX_DATA, which); + + // Dump network stats + dumpLine(pw, 0 /* uid */, category, GLOBAL_NETWORK_DATA, + mobileRxTotalBytes, mobileTxTotalBytes, wifiRxTotalBytes, wifiTxTotalBytes, + mobileRxTotalPackets, mobileTxTotalPackets, wifiRxTotalPackets, wifiTxTotalPackets); + // Dump misc stats dumpLine(pw, 0 /* uid */, category, MISC_DATA, screenOnTime / 1000, phoneOnTime / 1000, wifiOnTime / 1000, wifiRunningTime / 1000, bluetoothOnTime / 1000, - mobileRxTotal, mobileTxTotal, wifiRxTotal, wifiTxTotal, + mobileRxTotalBytes, mobileTxTotalBytes, wifiRxTotalBytes, wifiTxTotalBytes, fullWakeLockTimeTotal, partialWakeLockTimeTotal, - getInputEventCount(which)); + getInputEventCount(which), getMobileRadioActiveTime(rawRealtime, which), + getMobileRadioActiveAdjustedTime(which)); // Dump screen brightness stats Object[] args = new Object[NUM_SCREEN_BRIGHTNESS_BINS]; for (int i=0; i<NUM_SCREEN_BRIGHTNESS_BINS; i++) { - args[i] = getScreenBrightnessTime(i, batteryRealtime, which) / 1000; + args[i] = getScreenBrightnessTime(i, rawRealtime, which) / 1000; } dumpLine(pw, 0 /* uid */, category, SCREEN_BRIGHTNESS_DATA, args); // Dump signal strength stats args = new Object[SignalStrength.NUM_SIGNAL_STRENGTH_BINS]; for (int i=0; i<SignalStrength.NUM_SIGNAL_STRENGTH_BINS; i++) { - args[i] = getPhoneSignalStrengthTime(i, batteryRealtime, which) / 1000; + args[i] = getPhoneSignalStrengthTime(i, rawRealtime, which) / 1000; } dumpLine(pw, 0 /* uid */, category, SIGNAL_STRENGTH_TIME_DATA, args); dumpLine(pw, 0 /* uid */, category, SIGNAL_SCANNING_TIME_DATA, - getPhoneSignalScanningTime(batteryRealtime, which) / 1000); + getPhoneSignalScanningTime(rawRealtime, which) / 1000); for (int i=0; i<SignalStrength.NUM_SIGNAL_STRENGTH_BINS; i++) { args[i] = getPhoneSignalStrengthCount(i, which); } dumpLine(pw, 0 /* uid */, category, SIGNAL_STRENGTH_COUNT_DATA, args); - + // Dump network type stats args = new Object[NUM_DATA_CONNECTION_TYPES]; for (int i=0; i<NUM_DATA_CONNECTION_TYPES; i++) { - args[i] = getPhoneDataConnectionTime(i, batteryRealtime, which) / 1000; + args[i] = getPhoneDataConnectionTime(i, rawRealtime, which) / 1000; } dumpLine(pw, 0 /* uid */, category, DATA_CONNECTION_TIME_DATA, args); for (int i=0; i<NUM_DATA_CONNECTION_TYPES; i++) { args[i] = getPhoneDataConnectionCount(i, which); } dumpLine(pw, 0 /* uid */, category, DATA_CONNECTION_COUNT_DATA, args); - + + // Dump wifi state stats + args = new Object[NUM_WIFI_STATES]; + for (int i=0; i<NUM_WIFI_STATES; i++) { + args[i] = getWifiStateTime(i, rawRealtime, which) / 1000; + } + dumpLine(pw, 0 /* uid */, category, WIFI_STATE_TIME_DATA, args); + for (int i=0; i<NUM_WIFI_STATES; i++) { + args[i] = getWifiStateCount(i, which); + } + dumpLine(pw, 0 /* uid */, category, WIFI_STATE_COUNT_DATA, args); + + // Dump bluetooth state stats + args = new Object[NUM_BLUETOOTH_STATES]; + for (int i=0; i<NUM_BLUETOOTH_STATES; i++) { + args[i] = getBluetoothStateTime(i, rawRealtime, which) / 1000; + } + dumpLine(pw, 0 /* uid */, category, BLUETOOTH_STATE_TIME_DATA, args); + for (int i=0; i<NUM_BLUETOOTH_STATES; i++) { + args[i] = getBluetoothStateCount(i, which); + } + dumpLine(pw, 0 /* uid */, category, BLUETOOTH_STATE_COUNT_DATA, args); + if (which == STATS_SINCE_UNPLUGGED) { - dumpLine(pw, 0 /* uid */, category, BATTERY_LEVEL_DATA, getDischargeStartLevel(), + dumpLine(pw, 0 /* uid */, category, BATTERY_LEVEL_DATA, getDischargeStartLevel(), getDischargeCurrentLevel()); } @@ -1353,18 +1664,79 @@ public abstract class BatteryStats implements Parcelable { } if (reqUid < 0) { - Map<String, ? extends BatteryStats.Timer> kernelWakelocks = getKernelWakelockStats(); + Map<String, ? extends Timer> kernelWakelocks = getKernelWakelockStats(); if (kernelWakelocks.size() > 0) { - for (Map.Entry<String, ? extends BatteryStats.Timer> ent : kernelWakelocks.entrySet()) { + for (Map.Entry<String, ? extends Timer> ent : kernelWakelocks.entrySet()) { sb.setLength(0); - printWakeLockCheckin(sb, ent.getValue(), batteryRealtime, null, which, ""); - - dumpLine(pw, 0 /* uid */, category, KERNEL_WAKELOCK_DATA, ent.getKey(), + printWakeLockCheckin(sb, ent.getValue(), rawRealtime, null, which, ""); + dumpLine(pw, 0 /* uid */, category, KERNEL_WAKELOCK_DATA, ent.getKey(), sb.toString()); } } + Map<String, ? extends LongCounter> wakeupReasons = getWakeupReasonStats(); + if (wakeupReasons.size() > 0) { + for (Map.Entry<String, ? extends LongCounter> ent : wakeupReasons.entrySet()) { + dumpLine(pw, 0 /* uid */, category, WAKEUP_REASON_DATA, + "\"" + ent.getKey() + "\"", ent.getValue().getCountLocked(which)); + } + } } + BatteryStatsHelper helper = new BatteryStatsHelper(context); + helper.create(this); + helper.refreshStats(which, UserHandle.USER_ALL); + List<BatterySipper> sippers = helper.getUsageList(); + if (sippers != null && sippers.size() > 0) { + dumpLine(pw, 0 /* uid */, category, POWER_USE_SUMMARY_DATA, + BatteryStatsHelper.makemAh(helper.getPowerProfile().getBatteryCapacity()), + BatteryStatsHelper.makemAh(helper.getComputedPower()), + BatteryStatsHelper.makemAh(helper.getMinDrainedPower()), + BatteryStatsHelper.makemAh(helper.getMaxDrainedPower())); + for (int i=0; i<sippers.size(); i++) { + BatterySipper bs = sippers.get(i); + int uid = 0; + String label; + switch (bs.drainType) { + case IDLE: + label="idle"; + break; + case CELL: + label="cell"; + break; + case PHONE: + label="phone"; + break; + case WIFI: + label="wifi"; + break; + case BLUETOOTH: + label="blue"; + break; + case SCREEN: + label="scrn"; + break; + case APP: + uid = bs.uidObj.getUid(); + label = "uid"; + break; + case USER: + uid = UserHandle.getUid(bs.userId, 0); + label = "user"; + break; + case UNACCOUNTED: + label = "unacc"; + break; + case OVERCOUNTED: + label = "over"; + break; + default: + label = "???"; + } + dumpLine(pw, uid, category, POWER_USE_ITEM_DATA, label, + BatteryStatsHelper.makemAh(bs.value)); + } + } + for (int iu = 0; iu < NU; iu++) { final int uid = uidStats.keyAt(iu); if (reqUid >= 0 && uid != reqUid) { @@ -1372,16 +1744,28 @@ public abstract class BatteryStats implements Parcelable { } Uid u = uidStats.valueAt(iu); // Dump Network stats per uid, if any - long mobileRx = u.getNetworkActivityCount(NETWORK_MOBILE_RX_BYTES, which); - long mobileTx = u.getNetworkActivityCount(NETWORK_MOBILE_TX_BYTES, which); - long wifiRx = u.getNetworkActivityCount(NETWORK_WIFI_RX_BYTES, which); - long wifiTx = u.getNetworkActivityCount(NETWORK_WIFI_TX_BYTES, which); - long fullWifiLockOnTime = u.getFullWifiLockTime(batteryRealtime, which); - long wifiScanTime = u.getWifiScanTime(batteryRealtime, which); - long uidWifiRunningTime = u.getWifiRunningTime(batteryRealtime, which); - - if (mobileRx > 0 || mobileTx > 0 || wifiRx > 0 || wifiTx > 0) { - dumpLine(pw, uid, category, NETWORK_DATA, mobileRx, mobileTx, wifiRx, wifiTx); + long mobileBytesRx = u.getNetworkActivityBytes(NETWORK_MOBILE_RX_DATA, which); + long mobileBytesTx = u.getNetworkActivityBytes(NETWORK_MOBILE_TX_DATA, which); + long wifiBytesRx = u.getNetworkActivityBytes(NETWORK_WIFI_RX_DATA, which); + long wifiBytesTx = u.getNetworkActivityBytes(NETWORK_WIFI_TX_DATA, which); + long mobilePacketsRx = u.getNetworkActivityPackets(NETWORK_MOBILE_RX_DATA, which); + long mobilePacketsTx = u.getNetworkActivityPackets(NETWORK_MOBILE_TX_DATA, which); + long mobileActiveTime = u.getMobileRadioActiveTime(which); + int mobileActiveCount = u.getMobileRadioActiveCount(which); + long wifiPacketsRx = u.getNetworkActivityPackets(NETWORK_WIFI_RX_DATA, which); + long wifiPacketsTx = u.getNetworkActivityPackets(NETWORK_WIFI_TX_DATA, which); + long fullWifiLockOnTime = u.getFullWifiLockTime(rawRealtime, which); + long wifiScanTime = u.getWifiScanTime(rawRealtime, which); + long uidWifiRunningTime = u.getWifiRunningTime(rawRealtime, which); + + if (mobileBytesRx > 0 || mobileBytesTx > 0 || wifiBytesRx > 0 || wifiBytesTx > 0 + || mobilePacketsRx > 0 || mobilePacketsTx > 0 || wifiPacketsRx > 0 + || wifiPacketsTx > 0 || mobileActiveTime > 0 || mobileActiveCount > 0) { + dumpLine(pw, uid, category, NETWORK_DATA, mobileBytesRx, mobileBytesTx, + wifiBytesRx, wifiBytesTx, + mobilePacketsRx, mobilePacketsTx, + wifiPacketsRx, wifiPacketsTx, + mobileActiveTime, mobileActiveCount); } if (fullWifiLockOnTime != 0 || wifiScanTime != 0 @@ -1411,11 +1795,11 @@ public abstract class BatteryStats implements Parcelable { String linePrefix = ""; sb.setLength(0); linePrefix = printWakeLockCheckin(sb, wl.getWakeTime(WAKE_TYPE_FULL), - batteryRealtime, "f", which, linePrefix); + rawRealtime, "f", which, linePrefix); linePrefix = printWakeLockCheckin(sb, wl.getWakeTime(WAKE_TYPE_PARTIAL), - batteryRealtime, "p", which, linePrefix); + rawRealtime, "p", which, linePrefix); linePrefix = printWakeLockCheckin(sb, wl.getWakeTime(WAKE_TYPE_WINDOW), - batteryRealtime, "w", which, linePrefix); + rawRealtime, "w", which, linePrefix); // Only log if we had at lease one wakelock... if (sb.length() > 0) { @@ -1437,7 +1821,7 @@ public abstract class BatteryStats implements Parcelable { Timer timer = se.getSensorTime(); if (timer != null) { // Convert from microseconds to milliseconds with rounding - long totalTime = (timer.getTotalTimeLocked(batteryRealtime, which) + 500) / 1000; + long totalTime = (timer.getTotalTimeLocked(rawRealtime, which) + 500) / 1000; int count = timer.getCountLocked(which); if (totalTime != 0) { dumpLine(pw, uid, category, SENSOR_DATA, sensorNumber, totalTime, count); @@ -1449,7 +1833,7 @@ public abstract class BatteryStats implements Parcelable { Timer vibTimer = u.getVibratorOnTimer(); if (vibTimer != null) { // Convert from microseconds to milliseconds with rounding - long totalTime = (vibTimer.getTotalTimeLocked(batteryRealtime, which) + 500) / 1000; + long totalTime = (vibTimer.getTotalTimeLocked(rawRealtime, which) + 500) / 1000; int count = vibTimer.getCountLocked(which); if (totalTime != 0) { dumpLine(pw, uid, category, VIBRATOR_DATA, totalTime, count); @@ -1459,7 +1843,7 @@ public abstract class BatteryStats implements Parcelable { Timer fgTimer = u.getForegroundActivityTimer(); if (fgTimer != null) { // Convert from microseconds to milliseconds with rounding - long totalTime = (fgTimer.getTotalTimeLocked(batteryRealtime, which) + 500) / 1000; + long totalTime = (fgTimer.getTotalTimeLocked(rawRealtime, which) + 500) / 1000; int count = fgTimer.getCountLocked(which); if (totalTime != 0) { dumpLine(pw, uid, category, FOREGROUND_DATA, totalTime, count); @@ -1527,18 +1911,25 @@ public abstract class BatteryStats implements Parcelable { } } + private void printmAh(PrintWriter printer, double power) { + printer.print(BatteryStatsHelper.makemAh(power)); + } + @SuppressWarnings("unused") - public final void dumpLocked(PrintWriter pw, String prefix, final int which, int reqUid) { + public final void dumpLocked(Context context, PrintWriter pw, String prefix, final int which, + int reqUid) { final long rawUptime = SystemClock.uptimeMillis() * 1000; final long rawRealtime = SystemClock.elapsedRealtime() * 1000; final long batteryUptime = getBatteryUptime(rawUptime); - final long batteryRealtime = getBatteryRealtime(rawRealtime); final long whichBatteryUptime = computeBatteryUptime(rawUptime, which); final long whichBatteryRealtime = computeBatteryRealtime(rawRealtime, which); final long totalRealtime = computeRealtime(rawRealtime, which); final long totalUptime = computeUptime(rawUptime, which); - + final long whichBatteryScreenOffUptime = computeBatteryScreenOffUptime(rawUptime, which); + final long whichBatteryScreenOffRealtime = computeBatteryScreenOffRealtime(rawRealtime, + which); + StringBuilder sb = new StringBuilder(128); SparseArray<? extends Uid> uidStats = getUidStats(); @@ -1556,37 +1947,56 @@ public abstract class BatteryStats implements Parcelable { pw.println(sb.toString()); sb.setLength(0); sb.append(prefix); + sb.append(" Time on battery screen off: "); + formatTimeMs(sb, whichBatteryScreenOffRealtime / 1000); sb.append("("); + sb.append(formatRatioLocked(whichBatteryScreenOffRealtime, totalRealtime)); + sb.append(") realtime, "); + formatTimeMs(sb, whichBatteryScreenOffUptime / 1000); + sb.append("("); + sb.append(formatRatioLocked(whichBatteryScreenOffUptime, totalRealtime)); + sb.append(") uptime"); + pw.println(sb.toString()); + sb.setLength(0); + sb.append(prefix); sb.append(" Total run time: "); formatTimeMs(sb, totalRealtime / 1000); sb.append("realtime, "); formatTimeMs(sb, totalUptime / 1000); - sb.append("uptime, "); + sb.append("uptime"); pw.println(sb.toString()); - - final long screenOnTime = getScreenOnTime(batteryRealtime, which); - final long phoneOnTime = getPhoneOnTime(batteryRealtime, which); - final long wifiRunningTime = getGlobalWifiRunningTime(batteryRealtime, which); - final long wifiOnTime = getWifiOnTime(batteryRealtime, which); - final long bluetoothOnTime = getBluetoothOnTime(batteryRealtime, which); + pw.print(" Start clock time: "); + pw.println(DateFormat.format("yyyy-MM-dd-HH-mm-ss", getStartClockTime()).toString()); + + final long screenOnTime = getScreenOnTime(rawRealtime, which); + final long phoneOnTime = getPhoneOnTime(rawRealtime, which); + final long wifiRunningTime = getGlobalWifiRunningTime(rawRealtime, which); + final long wifiOnTime = getWifiOnTime(rawRealtime, which); + final long bluetoothOnTime = getBluetoothOnTime(rawRealtime, which); sb.setLength(0); sb.append(prefix); sb.append(" Screen on: "); formatTimeMs(sb, screenOnTime / 1000); sb.append("("); sb.append(formatRatioLocked(screenOnTime, whichBatteryRealtime)); - sb.append("), Input events: "); sb.append(getInputEventCount(which)); - sb.append(", Active phone call: "); formatTimeMs(sb, phoneOnTime / 1000); - sb.append("("); sb.append(formatRatioLocked(phoneOnTime, whichBatteryRealtime)); - sb.append(")"); + sb.append(") "); sb.append(getScreenOnCount(which)); + sb.append("x, Input events: "); sb.append(getInputEventCount(which)); pw.println(sb.toString()); + if (phoneOnTime != 0) { + sb.setLength(0); + sb.append(prefix); + sb.append(" Active phone call: "); formatTimeMs(sb, phoneOnTime / 1000); + sb.append("("); sb.append(formatRatioLocked(phoneOnTime, whichBatteryRealtime)); + sb.append(") "); sb.append(getPhoneOnCount(which)); + } sb.setLength(0); sb.append(prefix); - sb.append(" Screen brightnesses: "); + sb.append(" Screen brightnesses:"); boolean didOne = false; for (int i=0; i<NUM_SCREEN_BRIGHTNESS_BINS; i++) { - final long time = getScreenBrightnessTime(i, batteryRealtime, which); + final long time = getScreenBrightnessTime(i, rawRealtime, which); if (time == 0) { continue; } - if (didOne) sb.append(", "); + sb.append("\n "); + sb.append(prefix); didOne = true; sb.append(SCREEN_BRIGHTNESS_NAMES[i]); sb.append(" "); @@ -1595,70 +2005,17 @@ public abstract class BatteryStats implements Parcelable { sb.append(formatRatioLocked(time, screenOnTime)); sb.append(")"); } - if (!didOne) sb.append("No activity"); + if (!didOne) sb.append(" (no activity)"); pw.println(sb.toString()); - // Calculate total network and wakelock times across all uids. - long mobileRxTotal = 0; - long mobileTxTotal = 0; - long wifiRxTotal = 0; - long wifiTxTotal = 0; + // Calculate wakelock times across all uids. long fullWakeLockTimeTotalMicros = 0; long partialWakeLockTimeTotalMicros = 0; - final Comparator<TimerEntry> timerComparator = new Comparator<TimerEntry>() { - @Override - public int compare(TimerEntry lhs, TimerEntry rhs) { - long lhsTime = lhs.mTime; - long rhsTime = rhs.mTime; - if (lhsTime < rhsTime) { - return 1; - } - if (lhsTime > rhsTime) { - return -1; - } - return 0; - } - }; - - if (reqUid < 0) { - Map<String, ? extends BatteryStats.Timer> kernelWakelocks = getKernelWakelockStats(); - if (kernelWakelocks.size() > 0) { - final ArrayList<TimerEntry> timers = new ArrayList<TimerEntry>(); - for (Map.Entry<String, ? extends BatteryStats.Timer> ent : kernelWakelocks.entrySet()) { - BatteryStats.Timer timer = ent.getValue(); - long totalTimeMillis = computeWakeLock(timer, batteryRealtime, which); - if (totalTimeMillis > 0) { - timers.add(new TimerEntry(ent.getKey(), 0, timer, totalTimeMillis)); - } - } - Collections.sort(timers, timerComparator); - for (int i=0; i<timers.size(); i++) { - TimerEntry timer = timers.get(i); - String linePrefix = ": "; - sb.setLength(0); - sb.append(prefix); - sb.append(" Kernel Wake lock "); - sb.append(timer.mName); - linePrefix = printWakeLock(sb, timer.mTimer, batteryRealtime, null, - which, linePrefix); - if (!linePrefix.equals(": ")) { - sb.append(" realtime"); - // Only print out wake locks that were held - pw.println(sb.toString()); - } - } - } - } - final ArrayList<TimerEntry> timers = new ArrayList<TimerEntry>(); for (int iu = 0; iu < NU; iu++) { Uid u = uidStats.valueAt(iu); - mobileRxTotal += u.getNetworkActivityCount(NETWORK_MOBILE_RX_BYTES, which); - mobileTxTotal += u.getNetworkActivityCount(NETWORK_MOBILE_TX_BYTES, which); - wifiRxTotal += u.getNetworkActivityCount(NETWORK_WIFI_RX_BYTES, which); - wifiTxTotal += u.getNetworkActivityCount(NETWORK_WIFI_TX_BYTES, which); Map<String, ? extends BatteryStats.Uid.Wakelock> wakelocks = u.getWakelockStats(); if (wakelocks.size() > 0) { @@ -1669,13 +2026,13 @@ public abstract class BatteryStats implements Parcelable { Timer fullWakeTimer = wl.getWakeTime(WAKE_TYPE_FULL); if (fullWakeTimer != null) { fullWakeLockTimeTotalMicros += fullWakeTimer.getTotalTimeLocked( - batteryRealtime, which); + rawRealtime, which); } Timer partialWakeTimer = wl.getWakeTime(WAKE_TYPE_PARTIAL); if (partialWakeTimer != null) { long totalTimeMicros = partialWakeTimer.getTotalTimeLocked( - batteryRealtime, which); + rawRealtime, which); if (totalTimeMicros > 0) { if (reqUid < 0) { // Only show the ordered list of all wake @@ -1691,30 +2048,47 @@ public abstract class BatteryStats implements Parcelable { } } + long mobileRxTotalBytes = getNetworkActivityBytes(NETWORK_MOBILE_RX_DATA, which); + long mobileTxTotalBytes = getNetworkActivityBytes(NETWORK_MOBILE_TX_DATA, which); + long wifiRxTotalBytes = getNetworkActivityBytes(NETWORK_WIFI_RX_DATA, which); + long wifiTxTotalBytes = getNetworkActivityBytes(NETWORK_WIFI_TX_DATA, which); + long mobileRxTotalPackets = getNetworkActivityPackets(NETWORK_MOBILE_RX_DATA, which); + long mobileTxTotalPackets = getNetworkActivityPackets(NETWORK_MOBILE_TX_DATA, which); + long wifiRxTotalPackets = getNetworkActivityPackets(NETWORK_WIFI_RX_DATA, which); + long wifiTxTotalPackets = getNetworkActivityPackets(NETWORK_WIFI_TX_DATA, which); + + if (fullWakeLockTimeTotalMicros != 0) { + sb.setLength(0); + sb.append(prefix); + sb.append(" Total full wakelock time: "); formatTimeMsNoSpace(sb, + (fullWakeLockTimeTotalMicros + 500) / 1000); + pw.println(sb.toString()); + } + + if (partialWakeLockTimeTotalMicros != 0) { + sb.setLength(0); + sb.append(prefix); + sb.append(" Total partial wakelock time: "); formatTimeMsNoSpace(sb, + (partialWakeLockTimeTotalMicros + 500) / 1000); + pw.println(sb.toString()); + } + pw.print(prefix); - pw.print(" Mobile total received: "); pw.print(formatBytesLocked(mobileRxTotal)); - pw.print(", Total sent: "); pw.println(formatBytesLocked(mobileTxTotal)); - pw.print(prefix); - pw.print(" Wi-Fi total received: "); pw.print(formatBytesLocked(wifiRxTotal)); - pw.print(", Total sent: "); pw.println(formatBytesLocked(wifiTxTotal)); - sb.setLength(0); - sb.append(prefix); - sb.append(" Total full wakelock time: "); formatTimeMs(sb, - (fullWakeLockTimeTotalMicros + 500) / 1000); - sb.append(", Total partial wakelock time: "); formatTimeMs(sb, - (partialWakeLockTimeTotalMicros + 500) / 1000); - pw.println(sb.toString()); - + pw.print(" Mobile total received: "); pw.print(formatBytesLocked(mobileRxTotalBytes)); + pw.print(", sent: "); pw.print(formatBytesLocked(mobileTxTotalBytes)); + pw.print(" (packets received "); pw.print(mobileRxTotalPackets); + pw.print(", sent "); pw.print(mobileTxTotalPackets); pw.println(")"); sb.setLength(0); sb.append(prefix); - sb.append(" Signal levels: "); + sb.append(" Signal levels:"); didOne = false; for (int i=0; i<SignalStrength.NUM_SIGNAL_STRENGTH_BINS; i++) { - final long time = getPhoneSignalStrengthTime(i, batteryRealtime, which); + final long time = getPhoneSignalStrengthTime(i, rawRealtime, which); if (time == 0) { continue; } - if (didOne) sb.append(", "); + sb.append("\n "); + sb.append(prefix); didOne = true; sb.append(SignalStrength.SIGNAL_STRENGTH_NAMES[i]); sb.append(" "); @@ -1725,25 +2099,26 @@ public abstract class BatteryStats implements Parcelable { sb.append(getPhoneSignalStrengthCount(i, which)); sb.append("x"); } - if (!didOne) sb.append("No activity"); + if (!didOne) sb.append(" (no activity)"); pw.println(sb.toString()); sb.setLength(0); sb.append(prefix); sb.append(" Signal scanning time: "); - formatTimeMs(sb, getPhoneSignalScanningTime(batteryRealtime, which) / 1000); + formatTimeMsNoSpace(sb, getPhoneSignalScanningTime(rawRealtime, which) / 1000); pw.println(sb.toString()); sb.setLength(0); sb.append(prefix); - sb.append(" Radio types: "); + sb.append(" Radio types:"); didOne = false; for (int i=0; i<NUM_DATA_CONNECTION_TYPES; i++) { - final long time = getPhoneDataConnectionTime(i, batteryRealtime, which); + final long time = getPhoneDataConnectionTime(i, rawRealtime, which); if (time == 0) { continue; } - if (didOne) sb.append(", "); + sb.append("\n "); + sb.append(prefix); didOne = true; sb.append(DATA_CONNECTION_NAMES[i]); sb.append(" "); @@ -1754,28 +2129,112 @@ public abstract class BatteryStats implements Parcelable { sb.append(getPhoneDataConnectionCount(i, which)); sb.append("x"); } - if (!didOne) sb.append("No activity"); + if (!didOne) sb.append(" (no activity)"); pw.println(sb.toString()); sb.setLength(0); sb.append(prefix); - sb.append(" Radio data uptime when unplugged: "); - sb.append(getRadioDataUptime() / 1000); - sb.append(" ms"); + sb.append(" Mobile radio active time: "); + final long mobileActiveTime = getMobileRadioActiveTime(rawRealtime, which); + formatTimeMs(sb, mobileActiveTime / 1000); + sb.append("("); sb.append(formatRatioLocked(mobileActiveTime, whichBatteryRealtime)); + sb.append(") "); sb.append(getMobileRadioActiveCount(which)); + sb.append("x"); pw.println(sb.toString()); + final long mobileActiveUnknownTime = getMobileRadioActiveUnknownTime(which); + if (mobileActiveUnknownTime != 0) { + sb.setLength(0); + sb.append(prefix); + sb.append(" Mobile radio active unknown time: "); + formatTimeMs(sb, mobileActiveUnknownTime / 1000); + sb.append("("); + sb.append(formatRatioLocked(mobileActiveUnknownTime, whichBatteryRealtime)); + sb.append(") "); sb.append(getMobileRadioActiveUnknownCount(which)); + sb.append("x"); + pw.println(sb.toString()); + } + + final long mobileActiveAdjustedTime = getMobileRadioActiveAdjustedTime(which); + if (mobileActiveAdjustedTime != 0) { + sb.setLength(0); + sb.append(prefix); + sb.append(" Mobile radio active adjusted time: "); + formatTimeMs(sb, mobileActiveAdjustedTime / 1000); + sb.append("("); + sb.append(formatRatioLocked(mobileActiveAdjustedTime, whichBatteryRealtime)); + sb.append(")"); + pw.println(sb.toString()); + } + + pw.print(prefix); + pw.print(" Wi-Fi total received: "); pw.print(formatBytesLocked(wifiRxTotalBytes)); + pw.print(", sent: "); pw.print(formatBytesLocked(wifiTxTotalBytes)); + pw.print(" (packets received "); pw.print(wifiRxTotalPackets); + pw.print(", sent "); pw.print(wifiTxTotalPackets); pw.println(")"); sb.setLength(0); sb.append(prefix); sb.append(" Wifi on: "); formatTimeMs(sb, wifiOnTime / 1000); sb.append("("); sb.append(formatRatioLocked(wifiOnTime, whichBatteryRealtime)); sb.append("), Wifi running: "); formatTimeMs(sb, wifiRunningTime / 1000); sb.append("("); sb.append(formatRatioLocked(wifiRunningTime, whichBatteryRealtime)); - sb.append("), Bluetooth on: "); formatTimeMs(sb, bluetoothOnTime / 1000); + sb.append(")"); + pw.println(sb.toString()); + + sb.setLength(0); + sb.append(prefix); + sb.append(" Wifi states:"); + didOne = false; + for (int i=0; i<NUM_WIFI_STATES; i++) { + final long time = getWifiStateTime(i, rawRealtime, which); + if (time == 0) { + continue; + } + sb.append("\n "); + didOne = true; + sb.append(WIFI_STATE_NAMES[i]); + sb.append(" "); + formatTimeMs(sb, time/1000); + sb.append("("); + sb.append(formatRatioLocked(time, whichBatteryRealtime)); + sb.append(") "); + sb.append(getPhoneDataConnectionCount(i, which)); + sb.append("x"); + } + if (!didOne) sb.append(" (no activity)"); + pw.println(sb.toString()); + + sb.setLength(0); + sb.append(prefix); + sb.append(" Bluetooth on: "); formatTimeMs(sb, bluetoothOnTime / 1000); sb.append("("); sb.append(formatRatioLocked(bluetoothOnTime, whichBatteryRealtime)); sb.append(")"); pw.println(sb.toString()); - - pw.println(" "); + + sb.setLength(0); + sb.append(prefix); + sb.append(" Bluetooth states:"); + didOne = false; + for (int i=0; i<NUM_BLUETOOTH_STATES; i++) { + final long time = getBluetoothStateTime(i, rawRealtime, which); + if (time == 0) { + continue; + } + sb.append("\n "); + didOne = true; + sb.append(BLUETOOTH_STATE_NAMES[i]); + sb.append(" "); + formatTimeMs(sb, time/1000); + sb.append("("); + sb.append(formatRatioLocked(time, whichBatteryRealtime)); + sb.append(") "); + sb.append(getPhoneDataConnectionCount(i, which)); + sb.append("x"); + } + if (!didOne) sb.append(" (no activity)"); + pw.println(sb.toString()); + + pw.println(); if (which == STATS_SINCE_UNPLUGGED) { if (getIsOnBattery()) { @@ -1809,24 +2268,185 @@ public abstract class BatteryStats implements Parcelable { pw.println(); } - if (timers.size() > 0) { - Collections.sort(timers, timerComparator); - pw.print(prefix); pw.println(" All partial wake locks:"); - for (int i=0; i<timers.size(); i++) { - TimerEntry timer = timers.get(i); + BatteryStatsHelper helper = new BatteryStatsHelper(context); + helper.create(this); + helper.refreshStats(which, UserHandle.USER_ALL); + List<BatterySipper> sippers = helper.getUsageList(); + if (sippers != null && sippers.size() > 0) { + pw.print(prefix); pw.println(" Estimated power use (mAh):"); + pw.print(prefix); pw.print(" Capacity: "); + printmAh(pw, helper.getPowerProfile().getBatteryCapacity()); + pw.print(", Computed drain: "); printmAh(pw, helper.getComputedPower()); + pw.print(", Min drain: "); printmAh(pw, helper.getMinDrainedPower()); + pw.print(", Max drain: "); printmAh(pw, helper.getMaxDrainedPower()); + pw.println(); + for (int i=0; i<sippers.size(); i++) { + BatterySipper bs = sippers.get(i); + switch (bs.drainType) { + case IDLE: + pw.print(prefix); pw.print(" Idle: "); printmAh(pw, bs.value); + pw.println(); + break; + case CELL: + pw.print(prefix); pw.print(" Cell standby: "); printmAh(pw, bs.value); + pw.println(); + break; + case PHONE: + pw.print(prefix); pw.print(" Phone calls: "); printmAh(pw, bs.value); + pw.println(); + break; + case WIFI: + pw.print(prefix); pw.print(" Wifi: "); printmAh(pw, bs.value); + pw.println(); + break; + case BLUETOOTH: + pw.print(prefix); pw.print(" Bluetooth: "); printmAh(pw, bs.value); + pw.println(); + break; + case SCREEN: + pw.print(prefix); pw.print(" Screen: "); printmAh(pw, bs.value); + pw.println(); + break; + case APP: + pw.print(prefix); pw.print(" Uid "); + UserHandle.formatUid(pw, bs.uidObj.getUid()); + pw.print(": "); printmAh(pw, bs.value); pw.println(); + break; + case USER: + pw.print(prefix); pw.print(" User "); pw.print(bs.userId); + pw.print(": "); printmAh(pw, bs.value); pw.println(); + break; + case UNACCOUNTED: + pw.print(prefix); pw.print(" Unaccounted: "); printmAh(pw, bs.value); + pw.println(); + break; + case OVERCOUNTED: + pw.print(prefix); pw.print(" Over-counted: "); printmAh(pw, bs.value); + pw.println(); + break; + } + } + pw.println(); + } + + sippers = helper.getMobilemsppList(); + if (sippers != null && sippers.size() > 0) { + pw.print(prefix); pw.println(" Per-app mobile ms per packet:"); + long totalTime = 0; + for (int i=0; i<sippers.size(); i++) { + BatterySipper bs = sippers.get(i); sb.setLength(0); - sb.append(" Wake lock "); - UserHandle.formatUid(sb, timer.mId); - sb.append(" "); - sb.append(timer.mName); - printWakeLock(sb, timer.mTimer, batteryRealtime, null, which, ": "); - sb.append(" realtime"); + sb.append(prefix); sb.append(" Uid "); + UserHandle.formatUid(sb, bs.uidObj.getUid()); + sb.append(": "); sb.append(BatteryStatsHelper.makemAh(bs.mobilemspp)); + sb.append(" ("); sb.append(bs.mobileRxPackets+bs.mobileTxPackets); + sb.append(" packets over "); formatTimeMsNoSpace(sb, bs.mobileActive); + sb.append(") "); sb.append(bs.mobileActiveCount); sb.append("x"); pw.println(sb.toString()); + totalTime += bs.mobileActive; } - timers.clear(); + sb.setLength(0); + sb.append(prefix); + sb.append(" TOTAL TIME: "); + formatTimeMs(sb, totalTime); + sb.append("("); sb.append(formatRatioLocked(totalTime, whichBatteryRealtime)); + sb.append(")"); + pw.println(sb.toString()); pw.println(); } + final Comparator<TimerEntry> timerComparator = new Comparator<TimerEntry>() { + @Override + public int compare(TimerEntry lhs, TimerEntry rhs) { + long lhsTime = lhs.mTime; + long rhsTime = rhs.mTime; + if (lhsTime < rhsTime) { + return 1; + } + if (lhsTime > rhsTime) { + return -1; + } + return 0; + } + }; + + if (reqUid < 0) { + Map<String, ? extends BatteryStats.Timer> kernelWakelocks = getKernelWakelockStats(); + if (kernelWakelocks.size() > 0) { + final ArrayList<TimerEntry> ktimers = new ArrayList<TimerEntry>(); + for (Map.Entry<String, ? extends BatteryStats.Timer> ent : kernelWakelocks.entrySet()) { + BatteryStats.Timer timer = ent.getValue(); + long totalTimeMillis = computeWakeLock(timer, rawRealtime, which); + if (totalTimeMillis > 0) { + ktimers.add(new TimerEntry(ent.getKey(), 0, timer, totalTimeMillis)); + } + } + if (ktimers.size() > 0) { + Collections.sort(ktimers, timerComparator); + pw.print(prefix); pw.println(" All kernel wake locks:"); + for (int i=0; i<ktimers.size(); i++) { + TimerEntry timer = ktimers.get(i); + String linePrefix = ": "; + sb.setLength(0); + sb.append(prefix); + sb.append(" Kernel Wake lock "); + sb.append(timer.mName); + linePrefix = printWakeLock(sb, timer.mTimer, rawRealtime, null, + which, linePrefix); + if (!linePrefix.equals(": ")) { + sb.append(" realtime"); + // Only print out wake locks that were held + pw.println(sb.toString()); + } + } + pw.println(); + } + } + + if (timers.size() > 0) { + Collections.sort(timers, timerComparator); + pw.print(prefix); pw.println(" All partial wake locks:"); + for (int i=0; i<timers.size(); i++) { + TimerEntry timer = timers.get(i); + sb.setLength(0); + sb.append(" Wake lock "); + UserHandle.formatUid(sb, timer.mId); + sb.append(" "); + sb.append(timer.mName); + printWakeLock(sb, timer.mTimer, rawRealtime, null, which, ": "); + sb.append(" realtime"); + pw.println(sb.toString()); + } + timers.clear(); + pw.println(); + } + + Map<String, ? extends LongCounter> wakeupReasons = getWakeupReasonStats(); + if (wakeupReasons.size() > 0) { + pw.print(prefix); pw.println(" All wakeup reasons:"); + final ArrayList<TimerEntry> reasons = new ArrayList<TimerEntry>(); + for (Map.Entry<String, ? extends LongCounter> ent : wakeupReasons.entrySet()) { + BatteryStats.LongCounter counter = ent.getValue(); + reasons.add(new TimerEntry(ent.getKey(), 0, null, + ent.getValue().getCountLocked(which))); + } + Collections.sort(reasons, timerComparator); + for (int i=0; i<reasons.size(); i++) { + TimerEntry timer = reasons.get(i); + String linePrefix = ": "; + sb.setLength(0); + sb.append(prefix); + sb.append(" Wakeup reason "); + sb.append(timer.mName); + sb.append(": "); + formatTimeMs(sb, timer.mTime); + sb.append("realtime"); + pw.println(sb.toString()); + } + pw.println(); + } + } + for (int iu=0; iu<NU; iu++) { final int uid = uidStats.keyAt(iu); if (reqUid >= 0 && uid != reqUid && uid != Process.SYSTEM_UID) { @@ -1840,24 +2460,70 @@ public abstract class BatteryStats implements Parcelable { UserHandle.formatUid(pw, uid); pw.println(":"); boolean uidActivity = false; - - long mobileRxBytes = u.getNetworkActivityCount(NETWORK_MOBILE_RX_BYTES, which); - long mobileTxBytes = u.getNetworkActivityCount(NETWORK_MOBILE_TX_BYTES, which); - long wifiRxBytes = u.getNetworkActivityCount(NETWORK_WIFI_RX_BYTES, which); - long wifiTxBytes = u.getNetworkActivityCount(NETWORK_WIFI_TX_BYTES, which); - long fullWifiLockOnTime = u.getFullWifiLockTime(batteryRealtime, which); - long wifiScanTime = u.getWifiScanTime(batteryRealtime, which); - long uidWifiRunningTime = u.getWifiRunningTime(batteryRealtime, which); - - if (mobileRxBytes > 0 || mobileTxBytes > 0) { + + long mobileRxBytes = u.getNetworkActivityBytes(NETWORK_MOBILE_RX_DATA, which); + long mobileTxBytes = u.getNetworkActivityBytes(NETWORK_MOBILE_TX_DATA, which); + long wifiRxBytes = u.getNetworkActivityBytes(NETWORK_WIFI_RX_DATA, which); + long wifiTxBytes = u.getNetworkActivityBytes(NETWORK_WIFI_TX_DATA, which); + long mobileRxPackets = u.getNetworkActivityPackets(NETWORK_MOBILE_RX_DATA, which); + long mobileTxPackets = u.getNetworkActivityPackets(NETWORK_MOBILE_TX_DATA, which); + long uidMobileActiveTime = u.getMobileRadioActiveTime(which); + int uidMobileActiveCount = u.getMobileRadioActiveCount(which); + long wifiRxPackets = u.getNetworkActivityPackets(NETWORK_WIFI_RX_DATA, which); + long wifiTxPackets = u.getNetworkActivityPackets(NETWORK_WIFI_TX_DATA, which); + long fullWifiLockOnTime = u.getFullWifiLockTime(rawRealtime, which); + long wifiScanTime = u.getWifiScanTime(rawRealtime, which); + long uidWifiRunningTime = u.getWifiRunningTime(rawRealtime, which); + + if (mobileRxBytes > 0 || mobileTxBytes > 0 + || mobileRxPackets > 0 || mobileTxPackets > 0) { pw.print(prefix); pw.print(" Mobile network: "); pw.print(formatBytesLocked(mobileRxBytes)); pw.print(" received, "); - pw.print(formatBytesLocked(mobileTxBytes)); pw.println(" sent"); + pw.print(formatBytesLocked(mobileTxBytes)); + pw.print(" sent (packets "); pw.print(mobileRxPackets); + pw.print(" received, "); pw.print(mobileTxPackets); pw.println(" sent)"); + } + if (uidMobileActiveTime > 0 || uidMobileActiveCount > 0) { + sb.setLength(0); + sb.append(prefix); sb.append(" Mobile radio active: "); + formatTimeMs(sb, uidMobileActiveTime / 1000); + sb.append("("); + sb.append(formatRatioLocked(uidMobileActiveTime, mobileActiveTime)); + sb.append(") "); sb.append(uidMobileActiveCount); sb.append("x"); + long packets = mobileRxPackets + mobileTxPackets; + if (packets == 0) { + packets = 1; + } + sb.append(" @ "); + sb.append(BatteryStatsHelper.makemAh(uidMobileActiveTime / 1000 / (double)packets)); + sb.append(" mspp"); + pw.println(sb.toString()); } - if (wifiRxBytes > 0 || wifiTxBytes > 0) { + + if (wifiRxBytes > 0 || wifiTxBytes > 0 || wifiRxPackets > 0 || wifiTxPackets > 0) { pw.print(prefix); pw.print(" Wi-Fi network: "); pw.print(formatBytesLocked(wifiRxBytes)); pw.print(" received, "); - pw.print(formatBytesLocked(wifiTxBytes)); pw.println(" sent"); + pw.print(formatBytesLocked(wifiTxBytes)); + pw.print(" sent (packets "); pw.print(wifiRxPackets); + pw.print(" received, "); pw.print(wifiTxPackets); pw.println(" sent)"); + } + + if (fullWifiLockOnTime != 0 || wifiScanTime != 0 + || uidWifiRunningTime != 0) { + sb.setLength(0); + sb.append(prefix); sb.append(" Wifi Running: "); + formatTimeMs(sb, uidWifiRunningTime / 1000); + sb.append("("); sb.append(formatRatioLocked(uidWifiRunningTime, + whichBatteryRealtime)); sb.append(")\n"); + sb.append(prefix); sb.append(" Full Wifi Lock: "); + formatTimeMs(sb, fullWifiLockOnTime / 1000); + sb.append("("); sb.append(formatRatioLocked(fullWifiLockOnTime, + whichBatteryRealtime)); sb.append(")\n"); + sb.append(prefix); sb.append(" Wifi Scan: "); + formatTimeMs(sb, wifiScanTime / 1000); + sb.append("("); sb.append(formatRatioLocked(wifiScanTime, + whichBatteryRealtime)); sb.append(")"); + pw.println(sb.toString()); } if (u.hasUserActivity()) { @@ -1881,24 +2547,6 @@ public abstract class BatteryStats implements Parcelable { pw.println(sb.toString()); } } - - if (fullWifiLockOnTime != 0 || wifiScanTime != 0 - || uidWifiRunningTime != 0) { - sb.setLength(0); - sb.append(prefix); sb.append(" Wifi Running: "); - formatTimeMs(sb, uidWifiRunningTime / 1000); - sb.append("("); sb.append(formatRatioLocked(uidWifiRunningTime, - whichBatteryRealtime)); sb.append(")\n"); - sb.append(prefix); sb.append(" Full Wifi Lock: "); - formatTimeMs(sb, fullWifiLockOnTime / 1000); - sb.append("("); sb.append(formatRatioLocked(fullWifiLockOnTime, - whichBatteryRealtime)); sb.append(")\n"); - sb.append(prefix); sb.append(" Wifi Scan: "); - formatTimeMs(sb, wifiScanTime / 1000); - sb.append("("); sb.append(formatRatioLocked(wifiScanTime, - whichBatteryRealtime)); sb.append(")"); - pw.println(sb.toString()); - } Map<String, ? extends BatteryStats.Uid.Wakelock> wakelocks = u.getWakelockStats(); if (wakelocks.size() > 0) { @@ -1912,11 +2560,11 @@ public abstract class BatteryStats implements Parcelable { sb.append(prefix); sb.append(" Wake lock "); sb.append(ent.getKey()); - linePrefix = printWakeLock(sb, wl.getWakeTime(WAKE_TYPE_FULL), batteryRealtime, + linePrefix = printWakeLock(sb, wl.getWakeTime(WAKE_TYPE_FULL), rawRealtime, "full", which, linePrefix); - linePrefix = printWakeLock(sb, wl.getWakeTime(WAKE_TYPE_PARTIAL), batteryRealtime, + linePrefix = printWakeLock(sb, wl.getWakeTime(WAKE_TYPE_PARTIAL), rawRealtime, "partial", which, linePrefix); - linePrefix = printWakeLock(sb, wl.getWakeTime(WAKE_TYPE_WINDOW), batteryRealtime, + linePrefix = printWakeLock(sb, wl.getWakeTime(WAKE_TYPE_WINDOW), rawRealtime, "window", which, linePrefix); if (!linePrefix.equals(": ")) { sb.append(" realtime"); @@ -1926,11 +2574,11 @@ public abstract class BatteryStats implements Parcelable { count++; } totalFull += computeWakeLock(wl.getWakeTime(WAKE_TYPE_FULL), - batteryRealtime, which); + rawRealtime, which); totalPartial += computeWakeLock(wl.getWakeTime(WAKE_TYPE_PARTIAL), - batteryRealtime, which); + rawRealtime, which); totalWindow += computeWakeLock(wl.getWakeTime(WAKE_TYPE_WINDOW), - batteryRealtime, which); + rawRealtime, which); } if (count > 1) { if (totalFull != 0 || totalPartial != 0 || totalWindow != 0) { @@ -1986,7 +2634,7 @@ public abstract class BatteryStats implements Parcelable { if (timer != null) { // Convert from microseconds to milliseconds with rounding long totalTime = (timer.getTotalTimeLocked( - batteryRealtime, which) + 500) / 1000; + rawRealtime, which) + 500) / 1000; int count = timer.getCountLocked(which); //timer.logState(); if (totalTime != 0) { @@ -2010,7 +2658,7 @@ public abstract class BatteryStats implements Parcelable { if (vibTimer != null) { // Convert from microseconds to milliseconds with rounding long totalTime = (vibTimer.getTotalTimeLocked( - batteryRealtime, which) + 500) / 1000; + rawRealtime, which) + 500) / 1000; int count = vibTimer.getCountLocked(which); //timer.logState(); if (totalTime != 0) { @@ -2029,7 +2677,7 @@ public abstract class BatteryStats implements Parcelable { Timer fgTimer = u.getForegroundActivityTimer(); if (fgTimer != null) { // Convert from microseconds to milliseconds with rounding - long totalTime = (fgTimer.getTotalTimeLocked(batteryRealtime, which) + 500) / 1000; + long totalTime = (fgTimer.getTotalTimeLocked(rawRealtime, which) + 500) / 1000; int count = fgTimer.getCountLocked(which); if (totalTime != 0) { sb.setLength(0); @@ -2151,28 +2799,53 @@ public abstract class BatteryStats implements Parcelable { } } - static void printBitDescriptions(PrintWriter pw, int oldval, int newval, BitDescription[] descriptions) { + static void printBitDescriptions(PrintWriter pw, int oldval, int newval, HistoryTag wakelockTag, + BitDescription[] descriptions, boolean longNames) { int diff = oldval ^ newval; if (diff == 0) return; + boolean didWake = false; for (int i=0; i<descriptions.length; i++) { BitDescription bd = descriptions[i]; if ((diff&bd.mask) != 0) { + pw.print(longNames ? " " : ","); if (bd.shift < 0) { - pw.print((newval&bd.mask) != 0 ? " +" : " -"); - pw.print(bd.name); + pw.print((newval&bd.mask) != 0 ? "+" : "-"); + pw.print(longNames ? bd.name : bd.shortName); + if (bd.mask == HistoryItem.STATE_WAKE_LOCK_FLAG && wakelockTag != null) { + didWake = true; + pw.print("="); + if (longNames) { + UserHandle.formatUid(pw, wakelockTag.uid); + pw.print(":\""); + pw.print(wakelockTag.string); + pw.print("\""); + } else { + pw.print(wakelockTag.poolIdx); + } + } } else { - pw.print(" "); - pw.print(bd.name); + pw.print(longNames ? bd.name : bd.shortName); pw.print("="); int val = (newval&bd.mask)>>bd.shift; if (bd.values != null && val >= 0 && val < bd.values.length) { - pw.print(bd.values[val]); + pw.print(longNames? bd.values[val] : bd.shortValues[val]); } else { pw.print(val); } } } } + if (!didWake && wakelockTag != null) { + pw.print(longNames ? "wake_lock=" : "w="); + if (longNames) { + UserHandle.formatUid(pw, wakelockTag.uid); + pw.print(":\""); + pw.print(wakelockTag.string); + pw.print("\""); + } else { + pw.print(wakelockTag.poolIdx); + } + } } public void prepareForDumpLocked() { @@ -2180,51 +2853,103 @@ public abstract class BatteryStats implements Parcelable { public static class HistoryPrinter { int oldState = 0; + int oldState2 = 0; + int oldLevel = -1; int oldStatus = -1; int oldHealth = -1; int oldPlug = -1; int oldTemp = -1; int oldVolt = -1; - - public void printNextItem(PrintWriter pw, HistoryItem rec, long now) { - pw.print(" "); - TimeUtils.formatDuration(rec.time-now, pw, TimeUtils.HUNDRED_DAY_FIELD_LEN); - pw.print(" "); + long lastTime = -1; + + public void printNextItem(PrintWriter pw, HistoryItem rec, long now, boolean checkin, + boolean verbose) { + if (!checkin) { + pw.print(" "); + if (now >= 0) { + TimeUtils.formatDuration(rec.time-now, pw, TimeUtils.HUNDRED_DAY_FIELD_LEN); + } else { + TimeUtils.formatDuration(rec.time, pw, TimeUtils.HUNDRED_DAY_FIELD_LEN); + } + pw.print(" ("); + pw.print(rec.numReadInts); + pw.print(") "); + } else { + if (lastTime < 0) { + if (now >= 0) { + pw.print("@"); + pw.print(rec.time-now); + } else { + pw.print(rec.time); + } + } else { + pw.print(rec.time-lastTime); + } + lastTime = rec.time; + } if (rec.cmd == HistoryItem.CMD_START) { - pw.println(" START"); + if (checkin) { + pw.print(":"); + } + pw.println("START"); + } else if (rec.cmd == HistoryItem.CMD_CURRENT_TIME) { + if (checkin) { + pw.print(":"); + } + pw.print("TIME:"); + if (checkin) { + pw.println(rec.currentTime); + } else { + pw.print(" "); + pw.println(DateFormat.format("yyyy-MM-dd-HH-mm-ss", + rec.currentTime).toString()); + } } else if (rec.cmd == HistoryItem.CMD_OVERFLOW) { - pw.println(" *OVERFLOW*"); + if (checkin) { + pw.print(":"); + } + pw.println("*OVERFLOW*"); } else { - if (rec.batteryLevel < 10) pw.print("00"); - else if (rec.batteryLevel < 100) pw.print("0"); - pw.print(rec.batteryLevel); - pw.print(" "); - if (rec.states < 0x10) pw.print("0000000"); - else if (rec.states < 0x100) pw.print("000000"); - else if (rec.states < 0x1000) pw.print("00000"); - else if (rec.states < 0x10000) pw.print("0000"); - else if (rec.states < 0x100000) pw.print("000"); - else if (rec.states < 0x1000000) pw.print("00"); - else if (rec.states < 0x10000000) pw.print("0"); - pw.print(Integer.toHexString(rec.states)); + if (!checkin) { + if (rec.batteryLevel < 10) pw.print("00"); + else if (rec.batteryLevel < 100) pw.print("0"); + pw.print(rec.batteryLevel); + if (verbose) { + pw.print(" "); + if (rec.states < 0) ; + else if (rec.states < 0x10) pw.print("0000000"); + else if (rec.states < 0x100) pw.print("000000"); + else if (rec.states < 0x1000) pw.print("00000"); + else if (rec.states < 0x10000) pw.print("0000"); + else if (rec.states < 0x100000) pw.print("000"); + else if (rec.states < 0x1000000) pw.print("00"); + else if (rec.states < 0x10000000) pw.print("0"); + pw.print(Integer.toHexString(rec.states)); + } + } else { + if (oldLevel != rec.batteryLevel) { + oldLevel = rec.batteryLevel; + pw.print(",Bl="); pw.print(rec.batteryLevel); + } + } if (oldStatus != rec.batteryStatus) { oldStatus = rec.batteryStatus; - pw.print(" status="); + pw.print(checkin ? ",Bs=" : " status="); switch (oldStatus) { case BatteryManager.BATTERY_STATUS_UNKNOWN: - pw.print("unknown"); + pw.print(checkin ? "?" : "unknown"); break; case BatteryManager.BATTERY_STATUS_CHARGING: - pw.print("charging"); + pw.print(checkin ? "c" : "charging"); break; case BatteryManager.BATTERY_STATUS_DISCHARGING: - pw.print("discharging"); + pw.print(checkin ? "d" : "discharging"); break; case BatteryManager.BATTERY_STATUS_NOT_CHARGING: - pw.print("not-charging"); + pw.print(checkin ? "n" : "not-charging"); break; case BatteryManager.BATTERY_STATUS_FULL: - pw.print("full"); + pw.print(checkin ? "f" : "full"); break; default: pw.print(oldStatus); @@ -2233,25 +2958,28 @@ public abstract class BatteryStats implements Parcelable { } if (oldHealth != rec.batteryHealth) { oldHealth = rec.batteryHealth; - pw.print(" health="); + pw.print(checkin ? ",Bh=" : " health="); switch (oldHealth) { case BatteryManager.BATTERY_HEALTH_UNKNOWN: - pw.print("unknown"); + pw.print(checkin ? "?" : "unknown"); break; case BatteryManager.BATTERY_HEALTH_GOOD: - pw.print("good"); + pw.print(checkin ? "g" : "good"); break; case BatteryManager.BATTERY_HEALTH_OVERHEAT: - pw.print("overheat"); + pw.print(checkin ? "h" : "overheat"); break; case BatteryManager.BATTERY_HEALTH_DEAD: - pw.print("dead"); + pw.print(checkin ? "d" : "dead"); break; case BatteryManager.BATTERY_HEALTH_OVER_VOLTAGE: - pw.print("over-voltage"); + pw.print(checkin ? "v" : "over-voltage"); break; case BatteryManager.BATTERY_HEALTH_UNSPECIFIED_FAILURE: - pw.print("failure"); + pw.print(checkin ? "f" : "failure"); + break; + case BatteryManager.BATTERY_HEALTH_COLD: + pw.print(checkin ? "c" : "cold"); break; default: pw.print(oldHealth); @@ -2260,19 +2988,19 @@ public abstract class BatteryStats implements Parcelable { } if (oldPlug != rec.batteryPlugType) { oldPlug = rec.batteryPlugType; - pw.print(" plug="); + pw.print(checkin ? ",Bp=" : " plug="); switch (oldPlug) { case 0: - pw.print("none"); + pw.print(checkin ? "n" : "none"); break; case BatteryManager.BATTERY_PLUGGED_AC: - pw.print("ac"); + pw.print(checkin ? "a" : "ac"); break; case BatteryManager.BATTERY_PLUGGED_USB: - pw.print("usb"); + pw.print(checkin ? "u" : "usb"); break; case BatteryManager.BATTERY_PLUGGED_WIRELESS: - pw.print("wireless"); + pw.print(checkin ? "w" : "wireless"); break; default: pw.print(oldPlug); @@ -2281,139 +3009,252 @@ public abstract class BatteryStats implements Parcelable { } if (oldTemp != rec.batteryTemperature) { oldTemp = rec.batteryTemperature; - pw.print(" temp="); + pw.print(checkin ? ",Bt=" : " temp="); pw.print(oldTemp); } if (oldVolt != rec.batteryVoltage) { oldVolt = rec.batteryVoltage; - pw.print(" volt="); + pw.print(checkin ? ",Bv=" : " volt="); pw.print(oldVolt); } - printBitDescriptions(pw, oldState, rec.states, - HISTORY_STATE_DESCRIPTIONS); + printBitDescriptions(pw, oldState, rec.states, rec.wakelockTag, + HISTORY_STATE_DESCRIPTIONS, !checkin); + printBitDescriptions(pw, oldState2, rec.states2, null, + HISTORY_STATE2_DESCRIPTIONS, !checkin); + if (rec.wakeReasonTag != null) { + if (checkin) { + pw.print(",Wr="); + pw.print(rec.wakeReasonTag.poolIdx); + } else { + pw.print(" wake_reason="); + pw.print(rec.wakeReasonTag.uid); + pw.print(":\""); + pw.print(rec.wakeReasonTag.string); + pw.print("\""); + } + } + if (rec.eventCode != HistoryItem.EVENT_NONE) { + pw.print(checkin ? "," : " "); + if ((rec.eventCode&HistoryItem.EVENT_FLAG_START) != 0) { + pw.print("+"); + } else if ((rec.eventCode&HistoryItem.EVENT_FLAG_FINISH) != 0) { + pw.print("-"); + } + String[] eventNames = checkin ? HISTORY_EVENT_CHECKIN_NAMES + : HISTORY_EVENT_NAMES; + int idx = rec.eventCode & ~(HistoryItem.EVENT_FLAG_START + | HistoryItem.EVENT_FLAG_FINISH); + if (idx >= 0 && idx < eventNames.length) { + pw.print(eventNames[idx]); + } else { + pw.print(checkin ? "Ev" : "event"); + pw.print(idx); + } + pw.print("="); + if (checkin) { + pw.print(rec.eventTag.poolIdx); + } else { + UserHandle.formatUid(pw, rec.eventTag.uid); + pw.print(":\""); + pw.print(rec.eventTag.string); + pw.print("\""); + } + } pw.println(); + oldState = rec.states; } - oldState = rec.states; } + } - public void printNextItemCheckin(PrintWriter pw, HistoryItem rec, long now) { - pw.print(rec.time-now); - pw.print(","); - if (rec.cmd == HistoryItem.CMD_START) { - pw.print("start"); - } else if (rec.cmd == HistoryItem.CMD_OVERFLOW) { - pw.print("overflow"); - } else { - pw.print(rec.batteryLevel); - pw.print(","); - pw.print(rec.states); - pw.print(","); - pw.print(rec.batteryStatus); - pw.print(","); - pw.print(rec.batteryHealth); - pw.print(","); - pw.print(rec.batteryPlugType); - pw.print(","); - pw.print((int)rec.batteryTemperature); - pw.print(","); - pw.print((int)rec.batteryVoltage); - } + private void printSizeValue(PrintWriter pw, long size) { + float result = size; + String suffix = ""; + if (result >= 10*1024) { + suffix = "KB"; + result = result / 1024; } + if (result >= 10*1024) { + suffix = "MB"; + result = result / 1024; + } + if (result >= 10*1024) { + suffix = "GB"; + result = result / 1024; + } + if (result >= 10*1024) { + suffix = "TB"; + result = result / 1024; + } + if (result >= 10*1024) { + suffix = "PB"; + result = result / 1024; + } + pw.print((int)result); + pw.print(suffix); } + public static final int DUMP_UNPLUGGED_ONLY = 1<<0; + public static final int DUMP_CHARGED_ONLY = 1<<1; + public static final int DUMP_HISTORY_ONLY = 1<<2; + public static final int DUMP_INCLUDE_HISTORY = 1<<3; + public static final int DUMP_VERBOSE = 1<<4; + /** * Dumps a human-readable summary of the battery statistics to the given PrintWriter. * * @param pw a Printer to receive the dump output. */ @SuppressWarnings("unused") - public void dumpLocked(PrintWriter pw, boolean isUnpluggedOnly, int reqUid) { + public void dumpLocked(Context context, PrintWriter pw, int flags, int reqUid, long histStart) { prepareForDumpLocked(); - long now = getHistoryBaseTime() + SystemClock.elapsedRealtime(); + final boolean filtering = + (flags&(DUMP_HISTORY_ONLY|DUMP_UNPLUGGED_ONLY|DUMP_CHARGED_ONLY)) != 0; + + if ((flags&DUMP_HISTORY_ONLY) != 0 || !filtering) { + long now = getHistoryBaseTime() + SystemClock.elapsedRealtime(); + + final HistoryItem rec = new HistoryItem(); + final long historyTotalSize = getHistoryTotalSize(); + final long historyUsedSize = getHistoryUsedSize(); + if (startIteratingHistoryLocked()) { + try { + pw.print("Battery History ("); + pw.print((100*historyUsedSize)/historyTotalSize); + pw.print("% used, "); + printSizeValue(pw, historyUsedSize); + pw.print(" used of "); + printSizeValue(pw, historyTotalSize); + pw.print(", "); + pw.print(getHistoryStringPoolSize()); + pw.print(" strings using "); + printSizeValue(pw, getHistoryStringPoolBytes()); + pw.println("):"); + HistoryPrinter hprinter = new HistoryPrinter(); + long lastTime = -1; + while (getNextHistoryLocked(rec)) { + lastTime = rec.time; + if (rec.time >= histStart) { + hprinter.printNextItem(pw, rec, histStart >= 0 ? -1 : now, false, + (flags&DUMP_VERBOSE) != 0); + } + } + if (histStart >= 0) { + pw.print(" NEXT: "); pw.println(lastTime+1); + } + pw.println(); + } finally { + finishIteratingHistoryLocked(); + } + } - final HistoryItem rec = new HistoryItem(); - if (startIteratingHistoryLocked()) { - pw.println("Battery History:"); - HistoryPrinter hprinter = new HistoryPrinter(); - while (getNextHistoryLocked(rec)) { - hprinter.printNextItem(pw, rec, now); + if (startIteratingOldHistoryLocked()) { + try { + pw.println("Old battery History:"); + HistoryPrinter hprinter = new HistoryPrinter(); + while (getNextOldHistoryLocked(rec)) { + hprinter.printNextItem(pw, rec, now, false, (flags&DUMP_VERBOSE) != 0); + } + pw.println(); + } finally { + finishIteratingOldHistoryLocked(); + } } - finishIteratingHistoryLocked(); - pw.println(""); } - if (startIteratingOldHistoryLocked()) { - pw.println("Old battery History:"); - HistoryPrinter hprinter = new HistoryPrinter(); - while (getNextOldHistoryLocked(rec)) { - hprinter.printNextItem(pw, rec, now); - } - finishIteratingOldHistoryLocked(); - pw.println(""); + if (filtering && (flags&(DUMP_UNPLUGGED_ONLY|DUMP_CHARGED_ONLY)) == 0) { + return; } - - SparseArray<? extends Uid> uidStats = getUidStats(); - final int NU = uidStats.size(); - boolean didPid = false; - long nowRealtime = SystemClock.elapsedRealtime(); - for (int i=0; i<NU; i++) { - Uid uid = uidStats.valueAt(i); - SparseArray<? extends Uid.Pid> pids = uid.getPidStats(); - if (pids != null) { - for (int j=0; j<pids.size(); j++) { - Uid.Pid pid = pids.valueAt(j); - if (!didPid) { - pw.println("Per-PID Stats:"); - didPid = true; + + if (!filtering) { + SparseArray<? extends Uid> uidStats = getUidStats(); + final int NU = uidStats.size(); + boolean didPid = false; + long nowRealtime = SystemClock.elapsedRealtime(); + for (int i=0; i<NU; i++) { + Uid uid = uidStats.valueAt(i); + SparseArray<? extends Uid.Pid> pids = uid.getPidStats(); + if (pids != null) { + for (int j=0; j<pids.size(); j++) { + Uid.Pid pid = pids.valueAt(j); + if (!didPid) { + pw.println("Per-PID Stats:"); + didPid = true; + } + long time = pid.mWakeSumMs + (pid.mWakeNesting > 0 + ? (nowRealtime - pid.mWakeStartMs) : 0); + pw.print(" PID "); pw.print(pids.keyAt(j)); + pw.print(" wake time: "); + TimeUtils.formatDuration(time, pw); + pw.println(""); } - long time = pid.mWakeSum + (pid.mWakeStart != 0 - ? (nowRealtime - pid.mWakeStart) : 0); - pw.print(" PID "); pw.print(pids.keyAt(j)); - pw.print(" wake time: "); - TimeUtils.formatDuration(time, pw); - pw.println(""); } } - } - if (didPid) { - pw.println(""); + if (didPid) { + pw.println(""); + } } - if (!isUnpluggedOnly) { + if (!filtering || (flags&DUMP_CHARGED_ONLY) != 0) { pw.println("Statistics since last charge:"); pw.println(" System starts: " + getStartCount() + ", currently on battery: " + getIsOnBattery()); - dumpLocked(pw, "", STATS_SINCE_CHARGED, reqUid); + dumpLocked(context, pw, "", STATS_SINCE_CHARGED, reqUid); pw.println(""); } - pw.println("Statistics since last unplugged:"); - dumpLocked(pw, "", STATS_SINCE_UNPLUGGED, reqUid); + if (!filtering || (flags&DUMP_UNPLUGGED_ONLY) != 0) { + pw.println("Statistics since last unplugged:"); + dumpLocked(context, pw, "", STATS_SINCE_UNPLUGGED, reqUid); + } } @SuppressWarnings("unused") - public void dumpCheckinLocked( - PrintWriter pw, List<ApplicationInfo> apps, boolean isUnpluggedOnly, - boolean includeHistory) { + public void dumpCheckinLocked(Context context, PrintWriter pw, + List<ApplicationInfo> apps, int flags, long histStart) { prepareForDumpLocked(); long now = getHistoryBaseTime() + SystemClock.elapsedRealtime(); - if (includeHistory) { + final boolean filtering = + (flags&(DUMP_HISTORY_ONLY|DUMP_UNPLUGGED_ONLY|DUMP_CHARGED_ONLY)) != 0; + + if ((flags&DUMP_INCLUDE_HISTORY) != 0 || (flags&DUMP_HISTORY_ONLY) != 0) { final HistoryItem rec = new HistoryItem(); if (startIteratingHistoryLocked()) { - HistoryPrinter hprinter = new HistoryPrinter(); - while (getNextHistoryLocked(rec)) { - pw.print(BATTERY_STATS_CHECKIN_VERSION); pw.print(','); - pw.print(0); pw.print(','); - pw.print(HISTORY_DATA); pw.print(','); - hprinter.printNextItemCheckin(pw, rec, now); - pw.println(); + try { + for (int i=0; i<getHistoryStringPoolSize(); i++) { + pw.print(BATTERY_STATS_CHECKIN_VERSION); pw.print(','); + pw.print(HISTORY_STRING_POOL); pw.print(','); + pw.print(i); + pw.print(','); + pw.print(getHistoryTagPoolString(i)); + pw.print(','); + pw.print(getHistoryTagPoolUid(i)); + pw.println(); + } + HistoryPrinter hprinter = new HistoryPrinter(); + long lastTime = -1; + while (getNextHistoryLocked(rec)) { + lastTime = rec.time; + if (rec.time >= histStart) { + pw.print(BATTERY_STATS_CHECKIN_VERSION); pw.print(','); + pw.print(HISTORY_DATA); pw.print(','); + hprinter.printNextItem(pw, rec, histStart >= 0 ? -1 : now, true, false); + } + } + if (histStart >= 0) { + pw.print("NEXT: "); pw.println(lastTime+1); + } + } finally { + finishIteratingHistoryLocked(); } - finishIteratingHistoryLocked(); } } + if (filtering && (flags&(DUMP_UNPLUGGED_ONLY|DUMP_CHARGED_ONLY)) == 0) { + return; + } + if (apps != null) { SparseArray<ArrayList<String>> uids = new SparseArray<ArrayList<String>>(); for (int i=0; i<apps.size(); i++) { @@ -2441,12 +3282,11 @@ public abstract class BatteryStats implements Parcelable { } } } - if (isUnpluggedOnly) { - dumpCheckinLocked(pw, STATS_SINCE_UNPLUGGED, -1); + if (!filtering || (flags&DUMP_CHARGED_ONLY) != 0) { + dumpCheckinLocked(context, pw, STATS_SINCE_CHARGED, -1); } - else { - dumpCheckinLocked(pw, STATS_SINCE_CHARGED, -1); - dumpCheckinLocked(pw, STATS_SINCE_UNPLUGGED, -1); + if (!filtering || (flags&DUMP_UNPLUGGED_ONLY) != 0) { + dumpCheckinLocked(context, pw, STATS_SINCE_UNPLUGGED, -1); } } } diff --git a/core/java/android/os/Broadcaster.java b/core/java/android/os/Broadcaster.java index 96dc61a..70dcdd8 100644 --- a/core/java/android/os/Broadcaster.java +++ b/core/java/android/os/Broadcaster.java @@ -171,10 +171,10 @@ public class Broadcaster public void broadcast(Message msg) { synchronized (this) { - if (mReg == null) { - return; - } - + if (mReg == null) { + return; + } + int senderWhat = msg.what; Registration start = mReg; Registration r = start; diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java index fae2d52..63e15a9 100644 --- a/core/java/android/os/Build.java +++ b/core/java/android/os/Build.java @@ -479,6 +479,19 @@ public class Build { * Android 4.5: KitKat for watches, snacks on the run. */ public static final int KITKAT_WATCH = CUR_DEVELOPMENT; // STOPSHIP: update API level + + /** + * L! + * + * <p>Applications targeting this or a later release will get these + * new changes in behavior:</p> + * <ul> + * <li> {@link android.content.Context#bindService Context.bindService} now + * requires an explicit Intent, and will throw an exception if given an explicit + * Intent.</li> + * </ul> + */ + public static final int L = CUR_DEVELOPMENT; } /** The type of build, like "user" or "eng". */ diff --git a/core/java/android/os/CommonClock.java b/core/java/android/os/CommonClock.java index 3a1da97..2ecf317 100644 --- a/core/java/android/os/CommonClock.java +++ b/core/java/android/os/CommonClock.java @@ -15,17 +15,8 @@ */ package android.os; -import java.net.InetAddress; -import java.net.Inet4Address; -import java.net.Inet6Address; import java.net.InetSocketAddress; import java.util.NoSuchElementException; -import static libcore.io.OsConstants.*; - -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; import android.os.Binder; import android.os.CommonTimeUtils; import android.os.IBinder; diff --git a/core/java/android/os/CommonTimeConfig.java b/core/java/android/os/CommonTimeConfig.java index 3355ee3..1f9fab5 100644 --- a/core/java/android/os/CommonTimeConfig.java +++ b/core/java/android/os/CommonTimeConfig.java @@ -15,7 +15,6 @@ */ package android.os; -import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.NoSuchElementException; diff --git a/core/java/android/os/Debug.java b/core/java/android/os/Debug.java index 2de1204..18730b6 100644 --- a/core/java/android/os/Debug.java +++ b/core/java/android/os/Debug.java @@ -26,7 +26,6 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; -import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.Reader; import java.lang.reflect.Field; @@ -41,7 +40,6 @@ import org.apache.harmony.dalvik.ddmc.ChunkHandler; import org.apache.harmony.dalvik.ddmc.DdmServer; import dalvik.bytecode.OpcodeInfo; -import dalvik.bytecode.Opcodes; import dalvik.system.VMDebug; diff --git a/core/java/android/os/DropBoxManager.java b/core/java/android/os/DropBoxManager.java index e1c1678..27001dc 100644 --- a/core/java/android/os/DropBoxManager.java +++ b/core/java/android/os/DropBoxManager.java @@ -16,14 +16,11 @@ package android.os; -import android.util.Log; - import com.android.internal.os.IDropBoxManagerService; import java.io.ByteArrayInputStream; import java.io.Closeable; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.zip.GZIPInputStream; diff --git a/core/java/android/os/Environment.java b/core/java/android/os/Environment.java index b5413db..e96398a 100644 --- a/core/java/android/os/Environment.java +++ b/core/java/android/os/Environment.java @@ -16,14 +16,13 @@ package android.os; +import android.app.admin.DevicePolicyManager; import android.content.Context; import android.os.storage.IMountService; -import android.os.storage.StorageManager; import android.os.storage.StorageVolume; import android.text.TextUtils; import android.util.Log; -import com.android.internal.annotations.GuardedBy; import com.google.android.collect.Lists; import java.io.File; @@ -42,6 +41,7 @@ public class Environment { private static final String ENV_MEDIA_STORAGE = "MEDIA_STORAGE"; private static final String ENV_SECONDARY_STORAGE = "SECONDARY_STORAGE"; private static final String ENV_ANDROID_ROOT = "ANDROID_ROOT"; + private static final String ENV_OEM_ROOT = "OEM_ROOT"; /** {@hide} */ public static final String DIR_ANDROID = "Android"; @@ -56,6 +56,7 @@ public class Environment { public static final String DIRECTORY_ANDROID = DIR_ANDROID; private static final File DIR_ANDROID_ROOT = getDirectory(ENV_ANDROID_ROOT, "/system"); + private static final File DIR_OEM_ROOT = getDirectory(ENV_OEM_ROOT, "/oem"); private static final File DIR_MEDIA_STORAGE = getDirectory(ENV_MEDIA_STORAGE, "/data/media"); private static final String CANONCIAL_EMULATED_STORAGE_TARGET = getCanonicalPathOrNull( @@ -66,33 +67,6 @@ public class Environment { private static UserEnvironment sCurrentUser; private static boolean sUserRequired; - private static final Object sLock = new Object(); - - @GuardedBy("sLock") - private static volatile StorageVolume sPrimaryVolume; - - private static StorageVolume getPrimaryVolume() { - if (SystemProperties.getBoolean("config.disable_storage", false)) { - return null; - } - - if (sPrimaryVolume == null) { - synchronized (sLock) { - if (sPrimaryVolume == null) { - try { - IMountService mountService = IMountService.Stub.asInterface(ServiceManager - .getService("mount")); - final StorageVolume[] volumes = mountService.getVolumeList(); - sPrimaryVolume = StorageManager.getPrimaryVolume(volumes); - } catch (Exception e) { - Log.e(TAG, "couldn't talk to MountService", e); - } - } - } - } - return sPrimaryVolume; - } - static { initForCurrentUser(); } @@ -101,10 +75,6 @@ public class Environment { public static void initForCurrentUser() { final int userId = UserHandle.myUserId(); sCurrentUser = new UserEnvironment(userId); - - synchronized (sLock) { - sPrimaryVolume = null; - } } /** {@hide} */ @@ -237,13 +207,24 @@ public class Environment { } /** - * Gets the Android root directory. + * Return root of the "system" partition holding the core Android OS. + * Always present and mounted read-only. */ public static File getRootDirectory() { return DIR_ANDROID_ROOT; } /** + * Return root directory of the "oem" partition holding OEM customizations, + * if any. If present, the partition is mounted read-only. + * + * @hide + */ + public static File getOemDirectory() { + return DIR_OEM_ROOT; + } + + /** * Gets the system directory available for secure storage. * If Encrypted File system is enabled, it returns an encrypted directory (/data/secure/system). * Otherwise, it returns the unencrypted /data/system directory. @@ -603,28 +584,28 @@ public class Environment { * Unknown storage state, such as when a path isn't backed by known storage * media. * - * @see #getStorageState(File) + * @see #getExternalStorageState(File) */ public static final String MEDIA_UNKNOWN = "unknown"; /** * Storage state if the media is not present. * - * @see #getStorageState(File) + * @see #getExternalStorageState(File) */ public static final String MEDIA_REMOVED = "removed"; /** * Storage state if the media is present but not mounted. * - * @see #getStorageState(File) + * @see #getExternalStorageState(File) */ public static final String MEDIA_UNMOUNTED = "unmounted"; /** * Storage state if the media is present and being disk-checked. * - * @see #getStorageState(File) + * @see #getExternalStorageState(File) */ public static final String MEDIA_CHECKING = "checking"; @@ -632,7 +613,7 @@ public class Environment { * Storage state if the media is present but is blank or is using an * unsupported filesystem. * - * @see #getStorageState(File) + * @see #getExternalStorageState(File) */ public static final String MEDIA_NOFS = "nofs"; @@ -640,7 +621,7 @@ public class Environment { * Storage state if the media is present and mounted at its mount point with * read/write access. * - * @see #getStorageState(File) + * @see #getExternalStorageState(File) */ public static final String MEDIA_MOUNTED = "mounted"; @@ -648,7 +629,7 @@ public class Environment { * Storage state if the media is present and mounted at its mount point with * read-only access. * - * @see #getStorageState(File) + * @see #getExternalStorageState(File) */ public static final String MEDIA_MOUNTED_READ_ONLY = "mounted_ro"; @@ -656,14 +637,14 @@ public class Environment { * Storage state if the media is present not mounted, and shared via USB * mass storage. * - * @see #getStorageState(File) + * @see #getExternalStorageState(File) */ public static final String MEDIA_SHARED = "shared"; /** * Storage state if the media was removed before it was unmounted. * - * @see #getStorageState(File) + * @see #getExternalStorageState(File) */ public static final String MEDIA_BAD_REMOVAL = "bad_removal"; @@ -671,7 +652,7 @@ public class Environment { * Storage state if the media is present but cannot be mounted. Typically * this happens if the file system on the media is corrupted. * - * @see #getStorageState(File) + * @see #getExternalStorageState(File) */ public static final String MEDIA_UNMOUNTABLE = "unmountable"; @@ -687,7 +668,15 @@ public class Environment { */ public static String getExternalStorageState() { final File externalDir = sCurrentUser.getExternalDirsForApp()[0]; - return getStorageState(externalDir); + return getExternalStorageState(externalDir); + } + + /** + * @deprecated use {@link #getExternalStorageState(File)} + */ + @Deprecated + public static String getStorageState(File path) { + return getExternalStorageState(path); } /** @@ -700,59 +689,81 @@ public class Environment { * {@link #MEDIA_MOUNTED_READ_ONLY}, {@link #MEDIA_SHARED}, * {@link #MEDIA_BAD_REMOVAL}, or {@link #MEDIA_UNMOUNTABLE}. */ - public static String getStorageState(File path) { - final String rawPath; - try { - rawPath = path.getCanonicalPath(); - } catch (IOException e) { - Log.w(TAG, "Failed to resolve target path: " + e); - return Environment.MEDIA_UNKNOWN; - } - - try { + public static String getExternalStorageState(File path) { + final StorageVolume volume = getStorageVolume(path); + if (volume != null) { final IMountService mountService = IMountService.Stub.asInterface( ServiceManager.getService("mount")); - final StorageVolume[] volumes = mountService.getVolumeList(); - for (StorageVolume volume : volumes) { - if (rawPath.startsWith(volume.getPath())) { - return mountService.getVolumeState(volume.getPath()); - } + try { + return mountService.getVolumeState(volume.getPath()); + } catch (RemoteException e) { } - } catch (RemoteException e) { - Log.w(TAG, "Failed to find external storage state: " + e); } + return Environment.MEDIA_UNKNOWN; } /** * Returns whether the primary "external" storage device is removable. - * If true is returned, this device is for example an SD card that the - * user can remove. If false is returned, the storage is built into - * the device and can not be physically removed. * - * <p>See {@link #getExternalStorageDirectory()} for more information. + * @return true if the storage device can be removed (such as an SD card), + * or false if the storage device is built in and cannot be + * physically removed. */ public static boolean isExternalStorageRemovable() { - final StorageVolume primary = getPrimaryVolume(); - return (primary != null && primary.isRemovable()); + if (isStorageDisabled()) return false; + final File externalDir = sCurrentUser.getExternalDirsForApp()[0]; + return isExternalStorageRemovable(externalDir); } /** - * Returns whether the device has an external storage device which is - * emulated. If true, the device does not have real external storage, and the directory - * returned by {@link #getExternalStorageDirectory()} will be allocated using a portion of - * the internal storage system. + * Returns whether the storage device that provides the given path is + * removable. * - * <p>Certain system services, such as the package manager, use this - * to determine where to install an application. + * @return true if the storage device can be removed (such as an SD card), + * or false if the storage device is built in and cannot be + * physically removed. + * @throws IllegalArgumentException if the path is not a valid storage + * device. + */ + public static boolean isExternalStorageRemovable(File path) { + final StorageVolume volume = getStorageVolume(path); + if (volume != null) { + return volume.isRemovable(); + } else { + throw new IllegalArgumentException("Failed to find storage device at " + path); + } + } + + /** + * Returns whether the primary "external" storage device is emulated. If + * true, data stored on this device will be stored on a portion of the + * internal storage system. * - * <p>Emulated external storage may also be encrypted - see - * {@link android.app.admin.DevicePolicyManager#setStorageEncryption( - * android.content.ComponentName, boolean)} for additional details. + * @see DevicePolicyManager#setStorageEncryption(android.content.ComponentName, + * boolean) */ public static boolean isExternalStorageEmulated() { - final StorageVolume primary = getPrimaryVolume(); - return (primary != null && primary.isEmulated()); + if (isStorageDisabled()) return false; + final File externalDir = sCurrentUser.getExternalDirsForApp()[0]; + return isExternalStorageEmulated(externalDir); + } + + /** + * Returns whether the storage device that provides the given path is + * emulated. If true, data stored on this device will be stored on a portion + * of the internal storage system. + * + * @throws IllegalArgumentException if the path is not a valid storage + * device. + */ + public static boolean isExternalStorageEmulated(File path) { + final StorageVolume volume = getStorageVolume(path); + if (volume != null) { + return volume.isEmulated(); + } else { + throw new IllegalArgumentException("Failed to find storage device at " + path); + } } static File getDirectory(String variableName, String defaultPath) { @@ -815,6 +826,32 @@ public class Environment { return cur; } + private static boolean isStorageDisabled() { + return SystemProperties.getBoolean("config.disable_storage", false); + } + + private static StorageVolume getStorageVolume(File path) { + try { + path = path.getCanonicalFile(); + } catch (IOException e) { + return null; + } + + try { + final IMountService mountService = IMountService.Stub.asInterface( + ServiceManager.getService("mount")); + final StorageVolume[] volumes = mountService.getVolumeList(); + for (StorageVolume volume : volumes) { + if (FileUtils.contains(volume.getPathFile(), path)) { + return volume; + } + } + } catch (RemoteException e) { + } + + return null; + } + /** * If the given path exists on emulated external storage, return the * translated backing path hosted on internal storage. This bypasses any diff --git a/core/java/android/os/FileObserver.java b/core/java/android/os/FileObserver.java index d633486..4e705e0 100644 --- a/core/java/android/os/FileObserver.java +++ b/core/java/android/os/FileObserver.java @@ -18,10 +18,7 @@ package android.os; import android.util.Log; -import com.android.internal.os.RuntimeInit; - import java.lang.ref.WeakReference; -import java.util.ArrayList; import java.util.HashMap; /** diff --git a/core/java/android/os/FileUtils.java b/core/java/android/os/FileUtils.java index ff3e277..dc18dee 100644 --- a/core/java/android/os/FileUtils.java +++ b/core/java/android/os/FileUtils.java @@ -20,9 +20,7 @@ import android.util.Log; import android.util.Slog; import libcore.io.ErrnoException; -import libcore.io.IoUtils; import libcore.io.Libcore; -import libcore.io.OsConstants; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; @@ -328,14 +326,15 @@ public class FileUtils { * * @param minCount Always keep at least this many files. * @param minAge Always keep files younger than this age. + * @return if any files were deleted. */ - public static void deleteOlderFiles(File dir, int minCount, long minAge) { + public static boolean deleteOlderFiles(File dir, int minCount, long minAge) { if (minCount < 0 || minAge < 0) { throw new IllegalArgumentException("Constraints must be positive or 0"); } final File[] files = dir.listFiles(); - if (files == null) return; + if (files == null) return false; // Sort with newest files first Arrays.sort(files, new Comparator<File>() { @@ -346,15 +345,41 @@ public class FileUtils { }); // Keep at least minCount files + boolean deleted = false; for (int i = minCount; i < files.length; i++) { final File file = files[i]; // Keep files newer than minAge final long age = System.currentTimeMillis() - file.lastModified(); if (age > minAge) { - Log.d(TAG, "Deleting old file " + file); - file.delete(); + if (file.delete()) { + Log.d(TAG, "Deleted old file " + file); + deleted = true; + } } } + return deleted; + } + + /** + * Test if a file lives under the given directory, either as a direct child + * or a distant grandchild. + * <p> + * Both files <em>must</em> have been resolved using + * {@link File#getCanonicalFile()} to avoid symlink or path traversal + * attacks. + */ + public static boolean contains(File dir, File file) { + String dirPath = dir.getPath(); + String filePath = file.getPath(); + + if (dirPath.equals(filePath)) { + return true; + } + + if (!dirPath.endsWith("/")) { + dirPath += "/"; + } + return filePath.startsWith(dirPath); } } diff --git a/core/java/android/os/Handler.java b/core/java/android/os/Handler.java index e6886c4..44367f3 100644 --- a/core/java/android/os/Handler.java +++ b/core/java/android/os/Handler.java @@ -330,6 +330,7 @@ public class Handler { * Causes the Runnable r to be added to the message queue, to be run * at a specific time given by <var>uptimeMillis</var>. * <b>The time-base is {@link android.os.SystemClock#uptimeMillis}.</b> + * Time spent in deep sleep will add an additional delay to execution. * The runnable will be run on the thread to which this handler is attached. * * @param r The Runnable that will be executed. @@ -352,6 +353,7 @@ public class Handler { * Causes the Runnable r to be added to the message queue, to be run * at a specific time given by <var>uptimeMillis</var>. * <b>The time-base is {@link android.os.SystemClock#uptimeMillis}.</b> + * Time spent in deep sleep will add an additional delay to execution. * The runnable will be run on the thread to which this handler is attached. * * @param r The Runnable that will be executed. @@ -377,6 +379,8 @@ public class Handler { * after the specified amount of time elapses. * The runnable will be run on the thread to which this handler * is attached. + * <b>The time-base is {@link android.os.SystemClock#uptimeMillis}.</b> + * Time spent in deep sleep will add an additional delay to execution. * * @param r The Runnable that will be executed. * @param delayMillis The delay (in milliseconds) until the Runnable @@ -570,6 +574,7 @@ public class Handler { * Enqueue a message into the message queue after all pending messages * before the absolute time (in milliseconds) <var>uptimeMillis</var>. * <b>The time-base is {@link android.os.SystemClock#uptimeMillis}.</b> + * Time spent in deep sleep will add an additional delay to execution. * You will receive it in {@link #handleMessage}, in the thread attached * to this handler. * diff --git a/core/java/android/os/IBatteryPropertiesRegistrar.aidl b/core/java/android/os/IBatteryPropertiesRegistrar.aidl index 376f6c9..fd01802 100644 --- a/core/java/android/os/IBatteryPropertiesRegistrar.aidl +++ b/core/java/android/os/IBatteryPropertiesRegistrar.aidl @@ -17,6 +17,7 @@ package android.os; import android.os.IBatteryPropertiesListener; +import android.os.BatteryProperty; /** * {@hide} @@ -25,4 +26,5 @@ import android.os.IBatteryPropertiesListener; interface IBatteryPropertiesRegistrar { void registerListener(IBatteryPropertiesListener listener); void unregisterListener(IBatteryPropertiesListener listener); + int getProperty(in int id, out BatteryProperty prop); } diff --git a/core/java/android/os/IBinder.java b/core/java/android/os/IBinder.java index a2432d6..73a0f65 100644 --- a/core/java/android/os/IBinder.java +++ b/core/java/android/os/IBinder.java @@ -17,7 +17,6 @@ package android.os; import java.io.FileDescriptor; -import java.io.PrintWriter; /** * Base interface for a remotable object, the core part of a lightweight diff --git a/core/java/android/os/INetworkActivityListener.aidl b/core/java/android/os/INetworkActivityListener.aidl new file mode 100644 index 0000000..24e6e55 --- /dev/null +++ b/core/java/android/os/INetworkActivityListener.aidl @@ -0,0 +1,24 @@ +/* Copyright 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.os; + +/** + * @hide + */ +oneway interface INetworkActivityListener +{ + void onNetworkActive(); +} diff --git a/core/java/android/os/INetworkManagementService.aidl b/core/java/android/os/INetworkManagementService.aidl index f65b6ba..f5ff185 100644 --- a/core/java/android/os/INetworkManagementService.aidl +++ b/core/java/android/os/INetworkManagementService.aidl @@ -23,6 +23,7 @@ import android.net.LinkAddress; import android.net.NetworkStats; import android.net.RouteInfo; import android.net.wifi.WifiConfiguration; +import android.os.INetworkActivityListener; /** * @hide @@ -306,14 +307,12 @@ interface INetworkManagementService * reference-counting if an idletimer already exists for given * {@code iface}. * - * {@code label} usually represents the network type of {@code iface}. - * Caller should ensure that {@code label} for an {@code iface} remains the - * same for all calls to addIdleTimer. + * {@code type} is the type of the interface, such as TYPE_MOBILE. * * Every {@code addIdleTimer} should be paired with a * {@link removeIdleTimer} to cleanup when the network disconnects. */ - void addIdleTimer(String iface, int timeout, String label); + void addIdleTimer(String iface, int timeout, int type); /** * Removes idletimer for an interface. @@ -441,4 +440,19 @@ interface INetworkManagementService * Determine whether the clatd (464xlat) service has been started */ boolean isClatdStarted(); + + /** + * Start listening for mobile activity state changes. + */ + void registerNetworkActivityListener(INetworkActivityListener listener); + + /** + * Stop listening for mobile activity state changes. + */ + void unregisterNetworkActivityListener(INetworkActivityListener listener); + + /** + * Check whether the mobile radio is currently active. + */ + boolean isNetworkActive(); } diff --git a/core/java/android/os/IPowerManager.aidl b/core/java/android/os/IPowerManager.aidl index 92af1a5..6c7b08d 100644 --- a/core/java/android/os/IPowerManager.aidl +++ b/core/java/android/os/IPowerManager.aidl @@ -23,14 +23,17 @@ import android.os.WorkSource; interface IPowerManager { - // WARNING: The first four methods must remain the first three methods because their + // WARNING: The first five methods must remain the first five methods because their // transaction numbers must not change unless IPowerManager.cpp is also updated. - void acquireWakeLock(IBinder lock, int flags, String tag, String packageName, in WorkSource ws); - void acquireWakeLockWithUid(IBinder lock, int flags, String tag, String packageName, int uidtoblame); + void acquireWakeLock(IBinder lock, int flags, String tag, String packageName, in WorkSource ws, + String historyTag); + void acquireWakeLockWithUid(IBinder lock, int flags, String tag, String packageName, + int uidtoblame); void releaseWakeLock(IBinder lock, int flags); void updateWakeLockUids(IBinder lock, in int[] uids); + oneway void powerHint(int hintId, int data); - void updateWakeLockWorkSource(IBinder lock, in WorkSource ws); + void updateWakeLockWorkSource(IBinder lock, in WorkSource ws, String historyTag); boolean isWakeLockLevelSupported(int level); void userActivity(long time, int event, int flags); diff --git a/core/java/android/os/IUserManager.aidl b/core/java/android/os/IUserManager.aidl index 3c9d0d9..1192a45 100644 --- a/core/java/android/os/IUserManager.aidl +++ b/core/java/android/os/IUserManager.aidl @@ -28,11 +28,13 @@ import android.graphics.Bitmap; */ interface IUserManager { UserInfo createUser(in String name, int flags); + UserInfo createProfileForUser(in String name, int flags, int userHandle); boolean removeUser(int userHandle); void setUserName(int userHandle, String name); void setUserIcon(int userHandle, in Bitmap icon); Bitmap getUserIcon(int userHandle); List<UserInfo> getUsers(boolean excludeDying); + List<UserInfo> getProfiles(int userHandle); UserInfo getUserInfo(int userHandle); boolean isRestricted(); void setGuestEnabled(boolean enable); diff --git a/core/java/android/os/IVibratorService.aidl b/core/java/android/os/IVibratorService.aidl index 456ffb1..4d05e2d 100644 --- a/core/java/android/os/IVibratorService.aidl +++ b/core/java/android/os/IVibratorService.aidl @@ -20,8 +20,8 @@ package android.os; interface IVibratorService { boolean hasVibrator(); - void vibrate(int uid, String packageName, long milliseconds, IBinder token); - void vibratePattern(int uid, String packageName, in long[] pattern, int repeat, IBinder token); + void vibrate(int uid, String opPkg, long milliseconds, int streamHint, IBinder token); + void vibratePattern(int uid, String opPkg, in long[] pattern, int repeat, int streamHint, IBinder token); void cancelVibrate(IBinder token); } diff --git a/core/java/android/os/Looper.java b/core/java/android/os/Looper.java index 21e9f6b..6d7c9cf 100644 --- a/core/java/android/os/Looper.java +++ b/core/java/android/os/Looper.java @@ -18,7 +18,6 @@ package android.os; import android.util.Log; import android.util.Printer; -import android.util.PrefixPrinter; /** * Class used to run a message loop for a thread. Threads by default do @@ -150,7 +149,7 @@ public final class Looper { + msg.callback + " what=" + msg.what); } - msg.recycle(); + msg.recycleUnchecked(); } } diff --git a/core/java/android/os/Message.java b/core/java/android/os/Message.java index 51203a4..d30bbc1 100644 --- a/core/java/android/os/Message.java +++ b/core/java/android/os/Message.java @@ -71,7 +71,14 @@ public final class Message implements Parcelable { */ public Messenger replyTo; - /** If set message is in use */ + /** If set message is in use. + * This flag is set when the message is enqueued and remains set while it + * is delivered and afterwards when it is recycled. The flag is only cleared + * when a new message is created or obtained since that is the only time that + * applications are allowed to modify the contents of the message. + * + * It is an error to attempt to enqueue or recycle a message that is already in use. + */ /*package*/ static final int FLAG_IN_USE = 1 << 0; /** If set message is asynchronous */ @@ -86,9 +93,9 @@ public final class Message implements Parcelable { /*package*/ Bundle data; - /*package*/ Handler target; + /*package*/ Handler target; - /*package*/ Runnable callback; + /*package*/ Runnable callback; // sometimes we store linked lists of these things /*package*/ Message next; @@ -109,6 +116,7 @@ public final class Message implements Parcelable { Message m = sPool; sPool = m.next; m.next = null; + m.flags = 0; // clear in-use flag sPoolSize--; return m; } @@ -241,12 +249,38 @@ public final class Message implements Parcelable { } /** - * Return a Message instance to the global pool. You MUST NOT touch - * the Message after calling this function -- it has effectively been - * freed. + * Return a Message instance to the global pool. + * <p> + * You MUST NOT touch the Message after calling this function because it has + * effectively been freed. It is an error to recycle a message that is currently + * enqueued or that is in the process of being delivered to a Handler. + * </p> */ public void recycle() { - clearForRecycle(); + if (isInUse()) { + throw new IllegalStateException("This message cannot be recycled because it " + + "is still in use."); + } + recycleUnchecked(); + } + + /** + * Recycles a Message that may be in-use. + * Used internally by the MessageQueue and Looper when disposing of queued Messages. + */ + void recycleUnchecked() { + // Mark the message as in use while it remains in the recycled object pool. + // Clear out all other details. + flags = FLAG_IN_USE; + what = 0; + arg1 = 0; + arg2 = 0; + obj = null; + replyTo = null; + when = 0; + target = null; + callback = null; + data = null; synchronized (sPoolSync) { if (sPoolSize < MAX_POOL_SIZE) { @@ -402,19 +436,6 @@ public final class Message implements Parcelable { } } - /*package*/ void clearForRecycle() { - flags = 0; - what = 0; - arg1 = 0; - arg2 = 0; - obj = null; - replyTo = null; - when = 0; - target = null; - callback = null; - data = null; - } - /*package*/ boolean isInUse() { return ((flags & FLAG_IN_USE) == FLAG_IN_USE); } diff --git a/core/java/android/os/MessageQueue.java b/core/java/android/os/MessageQueue.java index 75f9813..01a23ce 100644 --- a/core/java/android/os/MessageQueue.java +++ b/core/java/android/os/MessageQueue.java @@ -16,7 +16,6 @@ package android.os; -import android.util.AndroidRuntimeException; import android.util.Log; import android.util.Printer; @@ -126,6 +125,14 @@ public final class MessageQueue { } Message next() { + // Return here if the message loop has already quit and been disposed. + // This can happen if the application tries to restart a looper after quit + // which is not supported. + final long ptr = mPtr; + if (ptr == 0) { + return null; + } + int pendingIdleHandlerCount = -1; // -1 only during first iteration int nextPollTimeoutMillis = 0; for (;;) { @@ -133,9 +140,7 @@ public final class MessageQueue { Binder.flushPendingCommands(); } - // We can assume mPtr != 0 because the loop is obviously still running. - // The looper will not call this method after the loop quits. - nativePollOnce(mPtr, nextPollTimeoutMillis); + nativePollOnce(ptr, nextPollTimeoutMillis); synchronized (this) { // Try to retrieve the next message. Return if found. @@ -163,7 +168,6 @@ public final class MessageQueue { } msg.next = null; if (false) Log.v("MessageQueue", "Returning message: " + msg); - msg.markInUse(); return msg; } } else { @@ -227,7 +231,7 @@ public final class MessageQueue { void quit(boolean safe) { if (!mQuitAllowed) { - throw new RuntimeException("Main thread not allowed to quit."); + throw new IllegalStateException("Main thread not allowed to quit."); } synchronized (this) { @@ -253,6 +257,7 @@ public final class MessageQueue { synchronized (this) { final int token = mNextBarrierToken++; final Message msg = Message.obtain(); + msg.markInUse(); msg.when = when; msg.arg1 = token; @@ -297,7 +302,7 @@ public final class MessageQueue { mMessages = p.next; needWake = mMessages == null || mMessages.target != null; } - p.recycle(); + p.recycleUnchecked(); // If the loop is quitting then it is already awake. // We can assume mPtr != 0 when mQuitting is false. @@ -308,21 +313,23 @@ public final class MessageQueue { } boolean enqueueMessage(Message msg, long when) { - if (msg.isInUse()) { - throw new AndroidRuntimeException(msg + " This message is already in use."); - } if (msg.target == null) { - throw new AndroidRuntimeException("Message must have a target."); + throw new IllegalArgumentException("Message must have a target."); + } + if (msg.isInUse()) { + throw new IllegalStateException(msg + " This message is already in use."); } synchronized (this) { if (mQuitting) { - RuntimeException e = new RuntimeException( + IllegalStateException e = new IllegalStateException( msg.target + " sending message to a Handler on a dead thread"); Log.w("MessageQueue", e.getMessage(), e); + msg.recycle(); return false; } + msg.markInUse(); msg.when = when; Message p = mMessages; boolean needWake; @@ -418,7 +425,7 @@ public final class MessageQueue { && (object == null || p.obj == object)) { Message n = p.next; mMessages = n; - p.recycle(); + p.recycleUnchecked(); p = n; } @@ -429,7 +436,7 @@ public final class MessageQueue { if (n.target == h && n.what == what && (object == null || n.obj == object)) { Message nn = n.next; - n.recycle(); + n.recycleUnchecked(); p.next = nn; continue; } @@ -452,7 +459,7 @@ public final class MessageQueue { && (object == null || p.obj == object)) { Message n = p.next; mMessages = n; - p.recycle(); + p.recycleUnchecked(); p = n; } @@ -463,7 +470,7 @@ public final class MessageQueue { if (n.target == h && n.callback == r && (object == null || n.obj == object)) { Message nn = n.next; - n.recycle(); + n.recycleUnchecked(); p.next = nn; continue; } @@ -486,7 +493,7 @@ public final class MessageQueue { && (object == null || p.obj == object)) { Message n = p.next; mMessages = n; - p.recycle(); + p.recycleUnchecked(); p = n; } @@ -496,7 +503,7 @@ public final class MessageQueue { if (n != null) { if (n.target == h && (object == null || n.obj == object)) { Message nn = n.next; - n.recycle(); + n.recycleUnchecked(); p.next = nn; continue; } @@ -510,7 +517,7 @@ public final class MessageQueue { Message p = mMessages; while (p != null) { Message n = p.next; - p.recycle(); + p.recycleUnchecked(); p = n; } mMessages = null; @@ -538,7 +545,7 @@ public final class MessageQueue { do { p = n; n = p.next; - p.recycle(); + p.recycleUnchecked(); } while (n != null); } } diff --git a/core/java/android/os/NullVibrator.java b/core/java/android/os/NullVibrator.java index ac6027f..7b870ac 100644 --- a/core/java/android/os/NullVibrator.java +++ b/core/java/android/os/NullVibrator.java @@ -16,8 +16,6 @@ package android.os; -import android.util.Log; - /** * Vibrator implementation that does nothing. * @@ -38,22 +36,11 @@ public class NullVibrator extends Vibrator { return false; } - @Override - public void vibrate(long milliseconds) { - } - - @Override - public void vibrate(long[] pattern, int repeat) { - if (repeat >= pattern.length) { - throw new ArrayIndexOutOfBoundsException(); - } - } - /** * @hide */ @Override - public void vibrate(int owningUid, String owningPackage, long milliseconds) { + public void vibrate(int uid, String opPkg, long milliseconds, int streamHint) { vibrate(milliseconds); } @@ -61,8 +48,11 @@ public class NullVibrator extends Vibrator { * @hide */ @Override - public void vibrate(int owningUid, String owningPackage, long[] pattern, int repeat) { - vibrate(pattern, repeat); + public void vibrate(int uid, String opPkg, long[] pattern, int repeat, + int streamHint) { + if (repeat >= pattern.length) { + throw new ArrayIndexOutOfBoundsException(); + } } @Override diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java index 7425f67..8e0ff08 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -29,6 +29,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; +import java.io.ObjectStreamClass; import java.io.Serializable; import java.lang.reflect.Field; import java.util.ArrayList; @@ -2067,7 +2068,7 @@ public final class Parcel { return readByte(); case VAL_SERIALIZABLE: - return readSerializable(); + return readSerializable(loader); case VAL_PARCELABLEARRAY: return readParcelableArray(loader); @@ -2204,6 +2205,10 @@ public final class Parcel { * wasn't found in the parcel. */ public final Serializable readSerializable() { + return readSerializable(null); + } + + private final Serializable readSerializable(final ClassLoader loader) { String name = readString(); if (name == null) { // For some reason we were unable to read the name of the Serializable (either there @@ -2215,14 +2220,27 @@ public final class Parcel { byte[] serializedData = createByteArray(); ByteArrayInputStream bais = new ByteArrayInputStream(serializedData); try { - ObjectInputStream ois = new ObjectInputStream(bais); + ObjectInputStream ois = new ObjectInputStream(bais) { + @Override + protected Class<?> resolveClass(ObjectStreamClass osClass) + throws IOException, ClassNotFoundException { + // try the custom classloader if provided + if (loader != null) { + Class<?> c = Class.forName(osClass.getName(), false, loader); + if (c != null) { + return c; + } + } + return super.resolveClass(osClass); + } + }; return (Serializable) ois.readObject(); } catch (IOException ioe) { throw new RuntimeException("Parcelable encountered " + "IOException reading a Serializable object (name = " + name + ")", ioe); } catch (ClassNotFoundException cnfe) { - throw new RuntimeException("Parcelable encountered" + + throw new RuntimeException("Parcelable encountered " + "ClassNotFoundException reading a Serializable object (name = " + name + ")", cnfe); } @@ -2234,6 +2252,7 @@ public final class Parcel { private static final HashMap<ClassLoader,HashMap<String,Parcelable.Creator>> mCreators = new HashMap<ClassLoader,HashMap<String,Parcelable.Creator>>(); + /** @hide for internal use only. */ static protected final Parcel obtain(int obj) { throw new UnsupportedOperationException(); } diff --git a/core/java/android/os/ParcelFileDescriptor.java b/core/java/android/os/ParcelFileDescriptor.java index 5273c20..86dc8b4 100644 --- a/core/java/android/os/ParcelFileDescriptor.java +++ b/core/java/android/os/ParcelFileDescriptor.java @@ -872,6 +872,8 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { */ @Override public void writeToParcel(Parcel out, int flags) { + // WARNING: This must stay in sync with Parcel::readParcelFileDescriptor() + // in frameworks/native/libs/binder/Parcel.cpp if (mWrapped != null) { try { mWrapped.writeToParcel(out, flags); @@ -897,6 +899,8 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { = new Parcelable.Creator<ParcelFileDescriptor>() { @Override public ParcelFileDescriptor createFromParcel(Parcel in) { + // WARNING: This must stay in sync with Parcel::writeParcelFileDescriptor() + // in frameworks/native/libs/binder/Parcel.cpp final FileDescriptor fd = in.readRawFileDescriptor(); FileDescriptor commChannel = null; if (in.readInt() != 0) { diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java index 646bfef..f8d7c3e 100644 --- a/core/java/android/os/PowerManager.java +++ b/core/java/android/os/PowerManager.java @@ -235,6 +235,13 @@ public final class PowerManager { public static final int ON_AFTER_RELEASE = 0x20000000; /** + * Wake lock flag: This wake lock is not important for logging events. If a later + * wake lock is acquired that is important, it will be considered the one to log. + * @hide + */ + public static final int UNIMPORTANT_FOR_LOGGING = 0x40000000; + + /** * Flag for {@link WakeLock#release release(int)} to defer releasing a * {@link #PROXIMITY_SCREEN_OFF_WAKE_LOCK} wake lock until the proximity sensor returns * a negative value. @@ -302,6 +309,18 @@ public final class PowerManager { */ public static final int GO_TO_SLEEP_REASON_TIMEOUT = 2; + /** + * The value to pass as the 'reason' argument to reboot() to + * reboot into recovery mode (for applying system updates, doing + * factory resets, etc.). + * <p> + * Requires the {@link android.Manifest.permission#RECOVERY} + * permission (in addition to + * {@link android.Manifest.permission#REBOOT}). + * </p> + */ + public static final String REBOOT_RECOVERY = "recovery"; + final Context mContext; final IPowerManager mService; final Handler mHandler; @@ -678,14 +697,15 @@ public final class PowerManager { * </p> */ public final class WakeLock { - private final int mFlags; - private final String mTag; + private int mFlags; + private String mTag; private final String mPackageName; private final IBinder mToken; private int mCount; private boolean mRefCounted = true; private boolean mHeld; private WorkSource mWorkSource; + private String mHistoryTag; private final Runnable mReleaser = new Runnable() { public void run() { @@ -772,7 +792,8 @@ public final class PowerManager { // been explicitly released by the keyguard. mHandler.removeCallbacks(mReleaser); try { - mService.acquireWakeLock(mToken, mFlags, mTag, mPackageName, mWorkSource); + mService.acquireWakeLock(mToken, mFlags, mTag, mPackageName, mWorkSource, + mHistoryTag); } catch (RemoteException e) { } mHeld = true; @@ -866,13 +887,29 @@ public final class PowerManager { if (changed && mHeld) { try { - mService.updateWakeLockWorkSource(mToken, mWorkSource); + mService.updateWakeLockWorkSource(mToken, mWorkSource, mHistoryTag); } catch (RemoteException e) { } } } } + /** @hide */ + public void setTag(String tag) { + mTag = tag; + } + + /** @hide */ + public void setHistoryTag(String tag) { + mHistoryTag = tag; + } + + /** @hide */ + public void setUnimportantForLogging(boolean state) { + if (state) mFlags |= UNIMPORTANT_FOR_LOGGING; + else mFlags &= ~UNIMPORTANT_FOR_LOGGING; + } + @Override public String toString() { synchronized (mToken) { diff --git a/core/java/android/os/Registrant.java b/core/java/android/os/Registrant.java index c1780b9..705cc5d 100644 --- a/core/java/android/os/Registrant.java +++ b/core/java/android/os/Registrant.java @@ -20,7 +20,6 @@ import android.os.Handler; import android.os.Message; import java.lang.ref.WeakReference; -import java.util.HashMap; /** @hide */ public class Registrant diff --git a/core/java/android/os/RegistrantList.java b/core/java/android/os/RegistrantList.java index 56b9e2b..9ab61f5 100644 --- a/core/java/android/os/RegistrantList.java +++ b/core/java/android/os/RegistrantList.java @@ -17,10 +17,8 @@ package android.os; import android.os.Handler; -import android.os.Message; import java.util.ArrayList; -import java.util.HashMap; /** @hide */ public class RegistrantList diff --git a/core/java/android/os/SystemProperties.java b/core/java/android/os/SystemProperties.java index 156600e..1479035 100644 --- a/core/java/android/os/SystemProperties.java +++ b/core/java/android/os/SystemProperties.java @@ -18,8 +18,6 @@ package android.os; import java.util.ArrayList; -import android.util.Log; - /** * Gives access to the system properties store. The system properties diff --git a/core/java/android/os/SystemService.java b/core/java/android/os/SystemService.java index f345271..41e7546 100644 --- a/core/java/android/os/SystemService.java +++ b/core/java/android/os/SystemService.java @@ -16,8 +16,6 @@ package android.os; -import android.util.Slog; - import com.google.android.collect.Maps; import java.util.HashMap; diff --git a/core/java/android/os/SystemVibrator.java b/core/java/android/os/SystemVibrator.java index 700f80d..8d9cf54 100644 --- a/core/java/android/os/SystemVibrator.java +++ b/core/java/android/os/SystemVibrator.java @@ -16,7 +16,6 @@ package android.os; -import android.app.ActivityThread; import android.content.Context; import android.util.Log; @@ -28,18 +27,16 @@ import android.util.Log; public class SystemVibrator extends Vibrator { private static final String TAG = "Vibrator"; - private final String mPackageName; private final IVibratorService mService; private final Binder mToken = new Binder(); public SystemVibrator() { - mPackageName = ActivityThread.currentPackageName(); mService = IVibratorService.Stub.asInterface( ServiceManager.getService("vibrator")); } public SystemVibrator(Context context) { - mPackageName = context.getOpPackageName(); + super(context); mService = IVibratorService.Stub.asInterface( ServiceManager.getService("vibrator")); } @@ -57,27 +54,17 @@ public class SystemVibrator extends Vibrator { return false; } - @Override - public void vibrate(long milliseconds) { - vibrate(Process.myUid(), mPackageName, milliseconds); - } - - @Override - public void vibrate(long[] pattern, int repeat) { - vibrate(Process.myUid(), mPackageName, pattern, repeat); - } - /** * @hide */ @Override - public void vibrate(int owningUid, String owningPackage, long milliseconds) { + public void vibrate(int uid, String opPkg, long milliseconds, int streamHint) { if (mService == null) { Log.w(TAG, "Failed to vibrate; no vibrator service."); return; } try { - mService.vibrate(owningUid, owningPackage, milliseconds, mToken); + mService.vibrate(uid, opPkg, milliseconds, streamHint, mToken); } catch (RemoteException e) { Log.w(TAG, "Failed to vibrate.", e); } @@ -87,7 +74,8 @@ public class SystemVibrator extends Vibrator { * @hide */ @Override - public void vibrate(int owningUid, String owningPackage, long[] pattern, int repeat) { + public void vibrate(int uid, String opPkg, long[] pattern, int repeat, + int streamHint) { if (mService == null) { Log.w(TAG, "Failed to vibrate; no vibrator service."); return; @@ -97,7 +85,8 @@ public class SystemVibrator extends Vibrator { // anyway if (repeat < pattern.length) { try { - mService.vibratePattern(owningUid, owningPackage, pattern, repeat, mToken); + mService.vibratePattern(uid, opPkg, pattern, repeat, streamHint, + mToken); } catch (RemoteException e) { Log.w(TAG, "Failed to vibrate.", e); } diff --git a/core/java/android/os/Trace.java b/core/java/android/os/Trace.java index 3249bcb..57ed979 100644 --- a/core/java/android/os/Trace.java +++ b/core/java/android/os/Trace.java @@ -16,8 +16,6 @@ package android.os; -import android.util.Log; - /** * Writes trace events to the system trace buffer. These trace events can be * collected and visualized using the Systrace tool. diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index a3752a1..8aef9bd 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -17,14 +17,19 @@ package android.os; import android.app.ActivityManagerNative; import android.content.Context; -import android.content.RestrictionEntry; import android.content.pm.UserInfo; import android.content.res.Resources; import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Bitmap.Config; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; import android.util.Log; import com.android.internal.R; +import java.util.ArrayList; import java.util.List; /** @@ -165,11 +170,13 @@ public class UserManager { /** * Returns whether the system supports multiple users. - * @return true if multiple users can be created, false if it is a single user device. + * @return true if multiple users can be created by user, false if it is a single user device. * @hide */ public static boolean supportsMultipleUsers() { - return getMaxSupportedUsers() > 1; + return getMaxSupportedUsers() > 1 + && SystemProperties.getBoolean("fw.show_multiuserui", + Resources.getSystem().getBoolean(R.bool.config_enableMultiUserUI)); } /** @@ -409,6 +416,27 @@ public class UserManager { } /** + * Creates a user with the specified name and options as a profile of another user. + * Requires {@link android.Manifest.permission#MANAGE_USERS} permission. + * + * @param name the user's name + * @param flags flags that identify the type of user and other properties. + * @see UserInfo + * @param userHandle new user will be a profile of this use. + * + * @return the UserInfo object for the created user, or null if the user could not be created. + * @hide + */ + public UserInfo createProfileForUser(String name, int flags, int userHandle) { + try { + return mService.createProfileForUser(name, flags, userHandle); + } catch (RemoteException re) { + Log.w(TAG, "Could not create a user", re); + return null; + } + } + + /** * Return the number of users currently created on the device. */ public int getUserCount() { @@ -432,9 +460,97 @@ public class UserManager { } /** - * Returns information for all users on this device. + * Returns list of the profiles of userHandle including + * userHandle itself. + * * Requires {@link android.Manifest.permission#MANAGE_USERS} permission. - * @param excludeDying specify if the list should exclude users being removed. + * @param userHandle profiles of this user will be returned. + * @return the list of profiles. + * @hide + */ + public List<UserInfo> getProfiles(int userHandle) { + try { + return mService.getProfiles(userHandle); + } catch (RemoteException re) { + Log.w(TAG, "Could not get user list", re); + return null; + } + } + + /** + * Returns a list of UserHandles for profiles associated with this user, including this user. + * + * @return A non-empty list of UserHandles associated with the calling user. + */ + public List<UserHandle> getUserProfiles() { + ArrayList<UserHandle> profiles = new ArrayList<UserHandle>(); + List<UserInfo> users = getProfiles(UserHandle.myUserId()); + for (UserInfo info : users) { + UserHandle userHandle = new UserHandle(info.id); + profiles.add(userHandle); + } + return profiles; + } + + /** + * If the target user is a managed profile of the calling user or the caller + * is itself a managed profile, then this returns a badged copy of the given + * icon to be able to distinguish it from the original icon. + * <P> + * If the original drawable is not a BitmapDrawable, then the original + * drawable is returned. + * </P> + * + * @param icon The icon to badge. + * @param user The target user. + * @return A drawable that combines the original icon and a badge as + * determined by the system. + */ + public Drawable getBadgedDrawableForUser(Drawable icon, UserHandle user) { + int badgeResId = getBadgeResIdForUser(user.getIdentifier()); + if (badgeResId == 0) { + return icon; + } else { + Drawable badgeIcon = mContext.getPackageManager() + .getDrawable("system", badgeResId, null); + return getMergedDrawable(icon, badgeIcon); + } + } + + private int getBadgeResIdForUser(int userHandle) { + // Return the framework-provided badge. + List<UserInfo> userProfiles = getProfiles(UserHandle.myUserId()); + for (UserInfo user : userProfiles) { + if (user.id == userHandle + && user.isManagedProfile()) { + return com.android.internal.R.drawable.ic_corp_badge; + } + } + return 0; + } + + private Drawable getMergedDrawable(Drawable icon, Drawable badge) { + final int width = icon.getIntrinsicWidth(); + final int height = icon.getIntrinsicHeight(); + Bitmap bitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + icon.setBounds(0, 0, width, height); + icon.draw(canvas); + badge.setBounds(0, 0, width, height); + badge.draw(canvas); + BitmapDrawable merged = new BitmapDrawable(bitmap); + if (icon instanceof BitmapDrawable) { + merged.setTargetDensity(((BitmapDrawable) icon).getBitmap().getDensity()); + } + return merged; + } + + /** + * Returns information for all users on this device. Requires + * {@link android.Manifest.permission#MANAGE_USERS} permission. + * + * @param excludeDying specify if the list should exclude users being + * removed. * @return the list of users that were created. * @hide */ @@ -565,6 +681,26 @@ public class UserManager { } /** + * Returns true if the user switcher should be shown, this will be if there + * are multiple users that aren't managed profiles. + * @hide + * @return true if user switcher should be shown. + */ + public boolean isUserSwitcherEnabled() { + List<UserInfo> users = getUsers(true); + if (users == null) { + return false; + } + int switchableUserCount = 0; + for (UserInfo user : users) { + if (user.supportsSwitchTo()) { + ++switchableUserCount; + } + } + return switchableUserCount > 1; + } + + /** * Returns a serial number on this device for a given userHandle. User handles can be recycled * when deleting and creating users, but serial numbers are not reused until the device is wiped. * @param userHandle diff --git a/core/java/android/os/Vibrator.java b/core/java/android/os/Vibrator.java index 5d55143..c1d4d4c 100644 --- a/core/java/android/os/Vibrator.java +++ b/core/java/android/os/Vibrator.java @@ -16,7 +16,9 @@ package android.os; +import android.app.ActivityThread; import android.content.Context; +import android.media.AudioManager; /** * Class that operates the vibrator on the device. @@ -28,10 +30,21 @@ import android.content.Context; * {@link Context#getSystemService} with {@link Context#VIBRATOR_SERVICE} as the argument. */ public abstract class Vibrator { + + private final String mPackageName; + /** * @hide to prevent subclassing from outside of the framework */ public Vibrator() { + mPackageName = ActivityThread.currentPackageName(); + } + + /** + * @hide to prevent subclassing from outside of the framework + */ + protected Vibrator(Context context) { + mPackageName = context.getOpPackageName(); } /** @@ -40,7 +53,7 @@ public abstract class Vibrator { * @return True if the hardware has a vibrator, else false. */ public abstract boolean hasVibrator(); - + /** * Vibrate constantly for the specified period of time. * <p>This method requires the caller to hold the permission @@ -48,7 +61,23 @@ public abstract class Vibrator { * * @param milliseconds The number of milliseconds to vibrate. */ - public abstract void vibrate(long milliseconds); + public void vibrate(long milliseconds) { + vibrate(milliseconds, AudioManager.USE_DEFAULT_STREAM_TYPE); + } + + /** + * Vibrate constantly for the specified period of time. + * <p>This method requires the caller to hold the permission + * {@link android.Manifest.permission#VIBRATE}. + * + * @param milliseconds The number of milliseconds to vibrate. + * @param streamHint An {@link AudioManager} stream type corresponding to the vibration type. + * For example, specify {@link AudioManager#STREAM_ALARM} for alarm vibrations or + * {@link AudioManager#STREAM_RING} for vibrations associated with incoming calls. + */ + public void vibrate(long milliseconds, int streamHint) { + vibrate(Process.myUid(), mPackageName, milliseconds, streamHint); + } /** * Vibrate with a given pattern. @@ -70,21 +99,52 @@ public abstract class Vibrator { * @param repeat the index into pattern at which to repeat, or -1 if * you don't want to repeat. */ - public abstract void vibrate(long[] pattern, int repeat); + public void vibrate(long[] pattern, int repeat) { + vibrate(pattern, repeat, AudioManager.USE_DEFAULT_STREAM_TYPE); + } + + /** + * Vibrate with a given pattern. + * + * <p> + * Pass in an array of ints that are the durations for which to turn on or off + * the vibrator in milliseconds. The first value indicates the number of milliseconds + * to wait before turning the vibrator on. The next value indicates the number of milliseconds + * for which to keep the vibrator on before turning it off. Subsequent values alternate + * between durations in milliseconds to turn the vibrator off or to turn the vibrator on. + * </p><p> + * To cause the pattern to repeat, pass the index into the pattern array at which + * to start the repeat, or -1 to disable repeating. + * </p> + * <p>This method requires the caller to hold the permission + * {@link android.Manifest.permission#VIBRATE}. + * + * @param pattern an array of longs of times for which to turn the vibrator on or off. + * @param repeat the index into pattern at which to repeat, or -1 if + * you don't want to repeat. + * @param streamHint An {@link AudioManager} stream type corresponding to the vibration type. + * For example, specify {@link AudioManager#STREAM_ALARM} for alarm vibrations or + * {@link AudioManager#STREAM_RING} for vibrations associated with incoming calls. + */ + public void vibrate(long[] pattern, int repeat, int streamHint) { + vibrate(Process.myUid(), mPackageName, pattern, repeat, streamHint); + } /** * @hide - * Like {@link #vibrate(long)}, but allowing the caller to specify that + * Like {@link #vibrate(long, int)}, but allowing the caller to specify that * the vibration is owned by someone else. */ - public abstract void vibrate(int owningUid, String owningPackage, long milliseconds); + public abstract void vibrate(int uid, String opPkg, + long milliseconds, int streamHint); /** * @hide - * Like {@link #vibrate(long[], int)}, but allowing the caller to specify that + * Like {@link #vibrate(long[], int, int)}, but allowing the caller to specify that * the vibration is owned by someone else. */ - public abstract void vibrate(int owningUid, String owningPackage, long[] pattern, int repeat); + public abstract void vibrate(int uid, String opPkg, + long[] pattern, int repeat, int streamHint); /** * Turn the vibrator off. diff --git a/core/java/android/os/storage/IMountService.java b/core/java/android/os/storage/IMountService.java index 51ba2f6..939cda9 100644 --- a/core/java/android/os/storage/IMountService.java +++ b/core/java/android/os/storage/IMountService.java @@ -625,12 +625,13 @@ public interface IMountService extends IInterface { return _result; } - public int encryptStorage(String password) throws RemoteException { + public int encryptStorage(int type, String password) throws RemoteException { Parcel _data = Parcel.obtain(); Parcel _reply = Parcel.obtain(); int _result; try { _data.writeInterfaceToken(DESCRIPTOR); + _data.writeInt(type); _data.writeString(password); mRemote.transact(Stub.TRANSACTION_encryptStorage, _data, _reply, 0); _reply.readException(); @@ -642,12 +643,13 @@ public interface IMountService extends IInterface { return _result; } - public int changeEncryptionPassword(String password) throws RemoteException { + public int changeEncryptionPassword(int type, String password) throws RemoteException { Parcel _data = Parcel.obtain(); Parcel _reply = Parcel.obtain(); int _result; try { _data.writeInterfaceToken(DESCRIPTOR); + _data.writeInt(type); _data.writeString(password); mRemote.transact(Stub.TRANSACTION_changeEncryptionPassword, _data, _reply, 0); _reply.readException(); @@ -677,6 +679,83 @@ public interface IMountService extends IInterface { return _result; } + public int getPasswordType() throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + int _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + mRemote.transact(Stub.TRANSACTION_getPasswordType, _data, _reply, 0); + _reply.readException(); + _result = _reply.readInt(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + public String getPassword() throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + String _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + mRemote.transact(Stub.TRANSACTION_getPassword, _data, _reply, 0); + _reply.readException(); + _result = _reply.readString(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + public void clearPassword() throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + mRemote.transact(Stub.TRANSACTION_clearPassword, _data, _reply, IBinder.FLAG_ONEWAY); + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + public void setField(String field, String data) throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(field); + _data.writeString(data); + mRemote.transact(Stub.TRANSACTION_setField, _data, _reply, IBinder.FLAG_ONEWAY); + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + public String getField(String field) throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + String _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(field); + mRemote.transact(Stub.TRANSACTION_getField, _data, _reply, 0); + _reply.readException(); + _result = _reply.readString(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + public StorageVolume[] getVolumeList() throws RemoteException { Parcel _data = Parcel.obtain(); Parcel _reply = Parcel.obtain(); @@ -829,6 +908,16 @@ public interface IMountService extends IInterface { static final int TRANSACTION_mkdirs = IBinder.FIRST_CALL_TRANSACTION + 34; + static final int TRANSACTION_getPasswordType = IBinder.FIRST_CALL_TRANSACTION + 35; + + static final int TRANSACTION_getPassword = IBinder.FIRST_CALL_TRANSACTION + 36; + + static final int TRANSACTION_clearPassword = IBinder.FIRST_CALL_TRANSACTION + 37; + + static final int TRANSACTION_setField = IBinder.FIRST_CALL_TRANSACTION + 38; + + static final int TRANSACTION_getField = IBinder.FIRST_CALL_TRANSACTION + 39; + /** * Cast an IBinder object into an IMountService interface, generating a * proxy if needed. @@ -1122,16 +1211,18 @@ public interface IMountService extends IInterface { } case TRANSACTION_encryptStorage: { data.enforceInterface(DESCRIPTOR); + int type = data.readInt(); String password = data.readString(); - int result = encryptStorage(password); + int result = encryptStorage(type, password); reply.writeNoException(); reply.writeInt(result); return true; } case TRANSACTION_changeEncryptionPassword: { data.enforceInterface(DESCRIPTOR); + int type = data.readInt(); String password = data.readString(); - int result = changeEncryptionPassword(password); + int result = changeEncryptionPassword(type, password); reply.writeNoException(); reply.writeInt(result); return true; @@ -1181,6 +1272,42 @@ public interface IMountService extends IInterface { reply.writeInt(result); return true; } + case TRANSACTION_getPasswordType: { + data.enforceInterface(DESCRIPTOR); + int result = getPasswordType(); + reply.writeNoException(); + reply.writeInt(result); + return true; + } + case TRANSACTION_getPassword: { + data.enforceInterface(DESCRIPTOR); + String result = getPassword(); + reply.writeNoException(); + reply.writeString(result); + return true; + } + case TRANSACTION_clearPassword: { + data.enforceInterface(DESCRIPTOR); + clearPassword(); + reply.writeNoException(); + return true; + } + case TRANSACTION_setField: { + data.enforceInterface(DESCRIPTOR); + String field = data.readString(); + String contents = data.readString(); + setField(field, contents); + reply.writeNoException(); + return true; + } + case TRANSACTION_getField: { + data.enforceInterface(DESCRIPTOR); + String field = data.readString(); + String contents = getField(field); + reply.writeNoException(); + reply.writeString(contents); + return true; + } } return super.onTransact(code, data, reply, flags); } @@ -1370,12 +1497,13 @@ public interface IMountService extends IInterface { /** * Encrypts storage. */ - public int encryptStorage(String password) throws RemoteException; + public int encryptStorage(int type, String password) throws RemoteException; /** * Changes the encryption password. */ - public int changeEncryptionPassword(String password) throws RemoteException; + public int changeEncryptionPassword(int type, String password) + throws RemoteException; /** * Verify the encryption password against the stored volume. This method @@ -1412,4 +1540,35 @@ public interface IMountService extends IInterface { * external storage data or OBB directory belonging to calling app. */ public int mkdirs(String callingPkg, String path) throws RemoteException; + + /** + * Determines the type of the encryption password + * @return PasswordType + */ + public int getPasswordType() throws RemoteException; + + /** + * Get password from vold + * @return password or empty string + */ + public String getPassword() throws RemoteException; + + /** + * Securely clear password from vold + */ + public void clearPassword() throws RemoteException; + + /** + * Set a field in the crypto header. + * @param field field to set + * @param contents contents to set in field + */ + public void setField(String field, String contents) throws RemoteException; + + /** + * Gets a field from the crypto header. + * @param field field to get + * @return contents of field + */ + public String getField(String field) throws RemoteException; } diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java index f5e728d..4963991 100644 --- a/core/java/android/os/storage/StorageManager.java +++ b/core/java/android/os/storage/StorageManager.java @@ -58,6 +58,24 @@ import java.util.concurrent.atomic.AtomicInteger; * argument of {@link android.content.Context#STORAGE_SERVICE}. */ public class StorageManager { + + /// Consts to match the password types in cryptfs.h + /** Master key is encrypted with a password. + */ + public static final int CRYPT_TYPE_PASSWORD = 0; + + /** Master key is encrypted with the default password. + */ + public static final int CRYPT_TYPE_DEFAULT = 1; + + /** Master key is encrypted with a pattern. + */ + public static final int CRYPT_TYPE_PATTERN = 2; + + /** Master key is encrypted with a pin. + */ + public static final int CRYPT_TYPE_PIN = 3; + private static final String TAG = "StorageManager"; private final ContentResolver mResolver; diff --git a/core/java/android/preference/CheckBoxPreference.java b/core/java/android/preference/CheckBoxPreference.java index 1536760..1ce98b8 100644 --- a/core/java/android/preference/CheckBoxPreference.java +++ b/core/java/android/preference/CheckBoxPreference.java @@ -34,11 +34,16 @@ import android.widget.Checkable; */ public class CheckBoxPreference extends TwoStatePreference { - public CheckBoxPreference(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.CheckBoxPreference, defStyle, 0); + public CheckBoxPreference(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public CheckBoxPreference( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + final TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.CheckBoxPreference, defStyleAttr, defStyleRes); setSummaryOn(a.getString(com.android.internal.R.styleable.CheckBoxPreference_summaryOn)); setSummaryOff(a.getString(com.android.internal.R.styleable.CheckBoxPreference_summaryOff)); setDisableDependentsState(a.getBoolean( diff --git a/core/java/android/preference/DialogPreference.java b/core/java/android/preference/DialogPreference.java index a643c8a..b65eac7 100644 --- a/core/java/android/preference/DialogPreference.java +++ b/core/java/android/preference/DialogPreference.java @@ -64,12 +64,13 @@ public abstract class DialogPreference extends Preference implements /** Which button was clicked. */ private int mWhichButtonClicked; - - public DialogPreference(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.DialogPreference, defStyle, 0); + + public DialogPreference( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + final TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.DialogPreference, defStyleAttr, defStyleRes); mDialogTitle = a.getString(com.android.internal.R.styleable.DialogPreference_dialogTitle); if (mDialogTitle == null) { // Fallback on the regular title of the preference @@ -83,13 +84,20 @@ public abstract class DialogPreference extends Preference implements mDialogLayoutResId = a.getResourceId(com.android.internal.R.styleable.DialogPreference_dialogLayout, mDialogLayoutResId); a.recycle(); - + } + + public DialogPreference(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); } public DialogPreference(Context context, AttributeSet attrs) { this(context, attrs, com.android.internal.R.attr.dialogPreferenceStyle); } - + + public DialogPreference(Context context) { + this(context, null); + } + /** * Sets the title of the dialog. This will be shown on subsequent dialogs. * @@ -161,7 +169,7 @@ public abstract class DialogPreference extends Preference implements * @param dialogIconRes The icon, as a resource ID. */ public void setDialogIcon(int dialogIconRes) { - mDialogIcon = getContext().getResources().getDrawable(dialogIconRes); + mDialogIcon = getContext().getDrawable(dialogIconRes); } /** diff --git a/core/java/android/preference/EditTextPreference.java b/core/java/android/preference/EditTextPreference.java index aa27627..ff37042 100644 --- a/core/java/android/preference/EditTextPreference.java +++ b/core/java/android/preference/EditTextPreference.java @@ -49,9 +49,9 @@ public class EditTextPreference extends DialogPreference { private EditText mEditText; private String mText; - - public EditTextPreference(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + + public EditTextPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); mEditText = new EditText(context, attrs); @@ -67,6 +67,10 @@ public class EditTextPreference extends DialogPreference { mEditText.setEnabled(true); } + public EditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + public EditTextPreference(Context context, AttributeSet attrs) { this(context, attrs, com.android.internal.R.attr.editTextPreferenceStyle); } diff --git a/core/java/android/preference/GenericInflater.java b/core/java/android/preference/GenericInflater.java index 3003290..7de7d1c 100644 --- a/core/java/android/preference/GenericInflater.java +++ b/core/java/android/preference/GenericInflater.java @@ -191,7 +191,7 @@ abstract class GenericInflater<T, P extends GenericInflater.Parent> { public void setFactory(Factory<T> factory) { if (mFactorySet) { throw new IllegalStateException("" + - "A factory has already been set on this inflater"); + "A factory has already been set on this inflater"); } if (factory == null) { throw new NullPointerException("Given factory can not be null"); diff --git a/core/java/android/preference/ListPreference.java b/core/java/android/preference/ListPreference.java index 9edf112..8081a54 100644 --- a/core/java/android/preference/ListPreference.java +++ b/core/java/android/preference/ListPreference.java @@ -42,12 +42,12 @@ public class ListPreference extends DialogPreference { private String mSummary; private int mClickedDialogEntryIndex; private boolean mValueSet; - - public ListPreference(Context context, AttributeSet attrs) { - super(context, attrs); - - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.ListPreference, 0, 0); + + public ListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.ListPreference, defStyleAttr, defStyleRes); mEntries = a.getTextArray(com.android.internal.R.styleable.ListPreference_entries); mEntryValues = a.getTextArray(com.android.internal.R.styleable.ListPreference_entryValues); a.recycle(); @@ -56,11 +56,19 @@ public class ListPreference extends DialogPreference { * in the Preference class. */ a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.Preference, 0, 0); + com.android.internal.R.styleable.Preference, defStyleAttr, defStyleRes); mSummary = a.getString(com.android.internal.R.styleable.Preference_summary); a.recycle(); } + public ListPreference(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ListPreference(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.dialogPreferenceStyle); + } + public ListPreference(Context context) { this(context, null); } diff --git a/core/java/android/preference/MultiCheckPreference.java b/core/java/android/preference/MultiCheckPreference.java index 6953075..57c906d 100644 --- a/core/java/android/preference/MultiCheckPreference.java +++ b/core/java/android/preference/MultiCheckPreference.java @@ -40,12 +40,13 @@ public class MultiCheckPreference extends DialogPreference { private boolean[] mSetValues; private boolean[] mOrigValues; private String mSummary; - - public MultiCheckPreference(Context context, AttributeSet attrs) { - super(context, attrs); - - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.ListPreference, 0, 0); + + public MultiCheckPreference( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.ListPreference, defStyleAttr, defStyleRes); mEntries = a.getTextArray(com.android.internal.R.styleable.ListPreference_entries); if (mEntries != null) { setEntries(mEntries); @@ -63,6 +64,14 @@ public class MultiCheckPreference extends DialogPreference { a.recycle(); } + public MultiCheckPreference(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public MultiCheckPreference(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.dialogPreferenceStyle); + } + public MultiCheckPreference(Context context) { this(context, null); } diff --git a/core/java/android/preference/MultiSelectListPreference.java b/core/java/android/preference/MultiSelectListPreference.java index 553ce80..6c4c20f 100644 --- a/core/java/android/preference/MultiSelectListPreference.java +++ b/core/java/android/preference/MultiSelectListPreference.java @@ -44,16 +44,26 @@ public class MultiSelectListPreference extends DialogPreference { private Set<String> mValues = new HashSet<String>(); private Set<String> mNewValues = new HashSet<String>(); private boolean mPreferenceChanged; - - public MultiSelectListPreference(Context context, AttributeSet attrs) { - super(context, attrs); - - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.MultiSelectListPreference, 0, 0); + + public MultiSelectListPreference( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + final TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.MultiSelectListPreference, defStyleAttr, + defStyleRes); mEntries = a.getTextArray(com.android.internal.R.styleable.MultiSelectListPreference_entries); mEntryValues = a.getTextArray(com.android.internal.R.styleable.MultiSelectListPreference_entryValues); a.recycle(); } + + public MultiSelectListPreference(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public MultiSelectListPreference(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.dialogPreferenceStyle); + } public MultiSelectListPreference(Context context) { this(context, null); diff --git a/core/java/android/preference/Preference.java b/core/java/android/preference/Preference.java index f7d1eb7..56d5617 100644 --- a/core/java/android/preference/Preference.java +++ b/core/java/android/preference/Preference.java @@ -188,30 +188,33 @@ public class Preference implements Comparable<Preference> { /** * Perform inflation from XML and apply a class-specific base style. This - * constructor of Preference allows subclasses to use their own base - * style when they are inflating. For example, a {@link CheckBoxPreference} + * constructor of Preference allows subclasses to use their own base style + * when they are inflating. For example, a {@link CheckBoxPreference} * constructor calls this version of the super class constructor and - * supplies {@code android.R.attr.checkBoxPreferenceStyle} for <var>defStyle</var>. - * This allows the theme's checkbox preference style to modify all of the base - * preference attributes as well as the {@link CheckBoxPreference} class's - * attributes. - * + * supplies {@code android.R.attr.checkBoxPreferenceStyle} for + * <var>defStyleAttr</var>. This allows the theme's checkbox preference + * style to modify all of the base preference attributes as well as the + * {@link CheckBoxPreference} class's attributes. + * * @param context The Context this is associated with, through which it can - * access the current theme, resources, {@link SharedPreferences}, - * etc. - * @param attrs The attributes of the XML tag that is inflating the preference. - * @param defStyle The default style to apply to this preference. If 0, no style - * will be applied (beyond what is included in the theme). This - * may either be an attribute resource, whose value will be - * retrieved from the current theme, or an explicit style - * resource. + * access the current theme, resources, + * {@link SharedPreferences}, etc. + * @param attrs The attributes of the XML tag that is inflating the + * preference. + * @param defStyleAttr An attribute in the current theme that contains a + * reference to a style resource that supplies default values for + * the view. Can be 0 to not look for defaults. + * @param defStyleRes A resource identifier of a style resource that + * supplies default values for the view, used only if + * defStyleAttr is 0 or can not be found in the theme. Can be 0 + * to not look for defaults. * @see #Preference(Context, AttributeSet) */ - public Preference(Context context, AttributeSet attrs, int defStyle) { + public Preference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { mContext = context; - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.Preference, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.Preference, defStyleAttr, defStyleRes); for (int i = a.getIndexCount(); i >= 0; i--) { int attr = a.getIndex(i); switch (attr) { @@ -281,6 +284,30 @@ public class Preference implements Comparable<Preference> { mCanRecycleLayout = false; } } + + /** + * Perform inflation from XML and apply a class-specific base style. This + * constructor of Preference allows subclasses to use their own base style + * when they are inflating. For example, a {@link CheckBoxPreference} + * constructor calls this version of the super class constructor and + * supplies {@code android.R.attr.checkBoxPreferenceStyle} for + * <var>defStyleAttr</var>. This allows the theme's checkbox preference + * style to modify all of the base preference attributes as well as the + * {@link CheckBoxPreference} class's attributes. + * + * @param context The Context this is associated with, through which it can + * access the current theme, resources, + * {@link SharedPreferences}, etc. + * @param attrs The attributes of the XML tag that is inflating the + * preference. + * @param defStyleAttr An attribute in the current theme that contains a + * reference to a style resource that supplies default values for + * the view. Can be 0 to not look for defaults. + * @see #Preference(Context, AttributeSet) + */ + public Preference(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } /** * Constructor that is called when inflating a Preference from XML. This is @@ -506,8 +533,7 @@ public class Preference implements Comparable<Preference> { * @see #onCreateView(ViewGroup) */ protected void onBindView(View view) { - final TextView titleView = (TextView) view.findViewById( - com.android.internal.R.id.title); + final TextView titleView = (TextView) view.findViewById(com.android.internal.R.id.title); if (titleView != null) { final CharSequence title = getTitle(); if (!TextUtils.isEmpty(title)) { @@ -530,11 +556,11 @@ public class Preference implements Comparable<Preference> { } } - ImageView imageView = (ImageView) view.findViewById(com.android.internal.R.id.icon); + final ImageView imageView = (ImageView) view.findViewById(com.android.internal.R.id.icon); if (imageView != null) { if (mIconResId != 0 || mIcon != null) { if (mIcon == null) { - mIcon = getContext().getResources().getDrawable(mIconResId); + mIcon = getContext().getDrawable(mIconResId); } if (mIcon != null) { imageView.setImageDrawable(mIcon); @@ -543,6 +569,11 @@ public class Preference implements Comparable<Preference> { imageView.setVisibility(mIcon != null ? View.VISIBLE : View.GONE); } + final View imageFrame = view.findViewById(com.android.internal.R.id.icon_frame); + if (imageFrame != null) { + imageFrame.setVisibility(mIcon != null ? View.VISIBLE : View.GONE); + } + if (mShouldDisableView) { setEnabledStateOnViews(view, isEnabled()); } @@ -667,7 +698,7 @@ public class Preference implements Comparable<Preference> { */ public void setIcon(int iconResId) { mIconResId = iconResId; - setIcon(mContext.getResources().getDrawable(iconResId)); + setIcon(mContext.getDrawable(iconResId)); } /** diff --git a/core/java/android/preference/PreferenceActivity.java b/core/java/android/preference/PreferenceActivity.java index 2ab5a91..0418049 100644 --- a/core/java/android/preference/PreferenceActivity.java +++ b/core/java/android/preference/PreferenceActivity.java @@ -33,7 +33,6 @@ import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; import android.util.AttributeSet; -import android.util.Log; import android.util.TypedValue; import android.util.Xml; import android.view.LayoutInflater; @@ -794,8 +793,8 @@ public abstract class PreferenceActivity extends ListActivity implements if ("header".equals(nodeName)) { Header header = new Header(); - TypedArray sa = getResources().obtainAttributes(attrs, - com.android.internal.R.styleable.PreferenceHeader); + TypedArray sa = obtainStyledAttributes( + attrs, com.android.internal.R.styleable.PreferenceHeader); header.id = sa.getResourceId( com.android.internal.R.styleable.PreferenceHeader_id, (int)HEADER_ID_UNDEFINED); @@ -1173,7 +1172,7 @@ public abstract class PreferenceActivity extends ListActivity implements } } - private void switchToHeaderInner(String fragmentName, Bundle args, int direction) { + private void switchToHeaderInner(String fragmentName, Bundle args) { getFragmentManager().popBackStack(BACK_STACK_PREFS, FragmentManager.POP_BACK_STACK_INCLUSIVE); if (!isValidFragment(fragmentName)) { @@ -1196,7 +1195,7 @@ public abstract class PreferenceActivity extends ListActivity implements */ public void switchToHeader(String fragmentName, Bundle args) { setSelectedHeader(null); - switchToHeaderInner(fragmentName, args, 0); + switchToHeaderInner(fragmentName, args); } /** @@ -1215,8 +1214,7 @@ public abstract class PreferenceActivity extends ListActivity implements if (header.fragment == null) { throw new IllegalStateException("can't switch to header that has no fragment"); } - int direction = mHeaders.indexOf(header) - mHeaders.indexOf(mCurHeader); - switchToHeaderInner(header.fragment, header.fragmentArguments, direction); + switchToHeaderInner(header.fragment, header.fragmentArguments); setSelectedHeader(header); } } diff --git a/core/java/android/preference/PreferenceCategory.java b/core/java/android/preference/PreferenceCategory.java index 229a96a..253481b 100644 --- a/core/java/android/preference/PreferenceCategory.java +++ b/core/java/android/preference/PreferenceCategory.java @@ -16,8 +16,6 @@ package android.preference; -import java.util.Map; - import android.content.Context; import android.util.AttributeSet; @@ -34,9 +32,14 @@ import android.util.AttributeSet; */ public class PreferenceCategory extends PreferenceGroup { private static final String TAG = "PreferenceCategory"; - - public PreferenceCategory(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + + public PreferenceCategory( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public PreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); } public PreferenceCategory(Context context, AttributeSet attrs) { diff --git a/core/java/android/preference/PreferenceFrameLayout.java b/core/java/android/preference/PreferenceFrameLayout.java index 75372aa..886338f 100644 --- a/core/java/android/preference/PreferenceFrameLayout.java +++ b/core/java/android/preference/PreferenceFrameLayout.java @@ -16,7 +16,6 @@ package android.preference; -import android.app.FragmentBreadCrumbs; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; @@ -45,10 +44,15 @@ public class PreferenceFrameLayout extends FrameLayout { this(context, attrs, com.android.internal.R.attr.preferenceFrameLayoutStyle); } - public PreferenceFrameLayout(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.PreferenceFrameLayout, defStyle, 0); + public PreferenceFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public PreferenceFrameLayout( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + final TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.PreferenceFrameLayout, defStyleAttr, defStyleRes); float density = context.getResources().getDisplayMetrics().density; int defaultBorderTop = (int) (density * DEFAULT_BORDER_TOP + 0.5f); diff --git a/core/java/android/preference/PreferenceGroup.java b/core/java/android/preference/PreferenceGroup.java index 5f8c78d..2d35b1b 100644 --- a/core/java/android/preference/PreferenceGroup.java +++ b/core/java/android/preference/PreferenceGroup.java @@ -55,19 +55,23 @@ public abstract class PreferenceGroup extends Preference implements GenericInfla private int mCurrentPreferenceOrder = 0; private boolean mAttachedToActivity = false; - - public PreferenceGroup(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + + public PreferenceGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); mPreferenceList = new ArrayList<Preference>(); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.PreferenceGroup, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.PreferenceGroup, defStyleAttr, defStyleRes); mOrderingAsAdded = a.getBoolean(com.android.internal.R.styleable.PreferenceGroup_orderingFromXml, mOrderingAsAdded); a.recycle(); } + public PreferenceGroup(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + public PreferenceGroup(Context context, AttributeSet attrs) { this(context, attrs, 0); } diff --git a/core/java/android/preference/PreferenceGroupAdapter.java b/core/java/android/preference/PreferenceGroupAdapter.java index 23d0a19..c2e1f51 100644 --- a/core/java/android/preference/PreferenceGroupAdapter.java +++ b/core/java/android/preference/PreferenceGroupAdapter.java @@ -45,8 +45,11 @@ import android.widget.ListView; * adapter, use {@link PreferenceCategoryAdapter} instead. * * @see PreferenceCategoryAdapter + * + * @hide */ -class PreferenceGroupAdapter extends BaseAdapter implements OnPreferenceChangeInternalListener { +public class PreferenceGroupAdapter extends BaseAdapter + implements OnPreferenceChangeInternalListener { private static final String TAG = "PreferenceGroupAdapter"; @@ -88,6 +91,8 @@ class PreferenceGroupAdapter extends BaseAdapter implements OnPreferenceChangeIn } }; + private int mActivatedPosition = -1; + private static class PreferenceLayout implements Comparable<PreferenceLayout> { private int resId; private int widgetResId; @@ -207,6 +212,10 @@ class PreferenceGroupAdapter extends BaseAdapter implements OnPreferenceChangeIn return this.getItem(position).getId(); } + public void setActivated(int position) { + mActivatedPosition = position; + } + public View getView(int position, View convertView, ViewGroup parent) { final Preference preference = this.getItem(position); // Build a PreferenceLayout to compare with known ones that are cacheable. @@ -217,8 +226,9 @@ class PreferenceGroupAdapter extends BaseAdapter implements OnPreferenceChangeIn if (Collections.binarySearch(mPreferenceLayouts, mTempPreferenceLayout) < 0) { convertView = null; } - - return preference.getView(convertView, parent); + View result = preference.getView(convertView, parent); + result.setActivated(position == mActivatedPosition); + return result; } @Override diff --git a/core/java/android/preference/PreferenceInflater.java b/core/java/android/preference/PreferenceInflater.java index c21aa18..727fbca 100644 --- a/core/java/android/preference/PreferenceInflater.java +++ b/core/java/android/preference/PreferenceInflater.java @@ -19,16 +19,13 @@ package android.preference; import com.android.internal.util.XmlUtils; import java.io.IOException; -import java.util.Map; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; -import android.app.AliasActivity; import android.content.Context; import android.content.Intent; import android.util.AttributeSet; -import android.util.Log; /** * The {@link PreferenceInflater} is used to inflate preference hierarchies from diff --git a/core/java/android/preference/PreferenceManager.java b/core/java/android/preference/PreferenceManager.java index 17f88f1..5c8c8e9 100644 --- a/core/java/android/preference/PreferenceManager.java +++ b/core/java/android/preference/PreferenceManager.java @@ -800,8 +800,10 @@ public class PreferenceManager { * Interface definition for a callback to be invoked when a * {@link Preference} in the hierarchy rooted at this {@link PreferenceScreen} is * clicked. + * + * @hide */ - interface OnPreferenceTreeClickListener { + public interface OnPreferenceTreeClickListener { /** * Called when a preference in the tree rooted at this * {@link PreferenceScreen} has been clicked. diff --git a/core/java/android/preference/PreferenceScreen.java b/core/java/android/preference/PreferenceScreen.java index db80676..b1317e6 100644 --- a/core/java/android/preference/PreferenceScreen.java +++ b/core/java/android/preference/PreferenceScreen.java @@ -27,7 +27,6 @@ import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.view.Window; -import android.widget.AbsListView; import android.widget.Adapter; import android.widget.AdapterView; import android.widget.ListAdapter; diff --git a/core/java/android/preference/RingtonePreference.java b/core/java/android/preference/RingtonePreference.java index 2ebf294..488a0c4 100644 --- a/core/java/android/preference/RingtonePreference.java +++ b/core/java/android/preference/RingtonePreference.java @@ -50,11 +50,11 @@ public class RingtonePreference extends Preference implements private int mRequestCode; - public RingtonePreference(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.RingtonePreference, defStyle, 0); + public RingtonePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + final TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.RingtonePreference, defStyleAttr, defStyleRes); mRingtoneType = a.getInt(com.android.internal.R.styleable.RingtonePreference_ringtoneType, RingtoneManager.TYPE_RINGTONE); mShowDefault = a.getBoolean(com.android.internal.R.styleable.RingtonePreference_showDefault, @@ -64,6 +64,10 @@ public class RingtonePreference extends Preference implements a.recycle(); } + public RingtonePreference(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + public RingtonePreference(Context context, AttributeSet attrs) { this(context, attrs, com.android.internal.R.attr.ringtonePreferenceStyle); } diff --git a/core/java/android/preference/SeekBarDialogPreference.java b/core/java/android/preference/SeekBarDialogPreference.java index 0e89b16..9a08827 100644 --- a/core/java/android/preference/SeekBarDialogPreference.java +++ b/core/java/android/preference/SeekBarDialogPreference.java @@ -32,8 +32,9 @@ public class SeekBarDialogPreference extends DialogPreference { private Drawable mMyIcon; - public SeekBarDialogPreference(Context context, AttributeSet attrs) { - super(context, attrs); + public SeekBarDialogPreference( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); setDialogLayoutResource(com.android.internal.R.layout.seekbar_dialog); createActionButtons(); @@ -43,6 +44,18 @@ public class SeekBarDialogPreference extends DialogPreference { setDialogIcon(null); } + public SeekBarDialogPreference(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public SeekBarDialogPreference(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.dialogPreferenceStyle); + } + + public SeekBarDialogPreference(Context context) { + this(context, null); + } + // Allow subclasses to override the action buttons public void createActionButtons() { setPositiveButtonText(android.R.string.ok); diff --git a/core/java/android/preference/SeekBarPreference.java b/core/java/android/preference/SeekBarPreference.java index 7133d3a..e32890d 100644 --- a/core/java/android/preference/SeekBarPreference.java +++ b/core/java/android/preference/SeekBarPreference.java @@ -37,15 +37,20 @@ public class SeekBarPreference extends Preference private boolean mTrackingTouch; public SeekBarPreference( - Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.ProgressBar, defStyle, 0); + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.ProgressBar, defStyleAttr, defStyleRes); setMax(a.getInt(com.android.internal.R.styleable.ProgressBar_max, mMax)); a.recycle(); setLayoutResource(com.android.internal.R.layout.preference_widget_seekbar); } + public SeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + public SeekBarPreference(Context context, AttributeSet attrs) { this(context, attrs, 0); } diff --git a/core/java/android/preference/SwitchPreference.java b/core/java/android/preference/SwitchPreference.java index 8bac6bd..76ef544 100644 --- a/core/java/android/preference/SwitchPreference.java +++ b/core/java/android/preference/SwitchPreference.java @@ -60,13 +60,19 @@ public class SwitchPreference extends TwoStatePreference { * * @param context The Context that will style this preference * @param attrs Style attributes that differ from the default - * @param defStyle Theme attribute defining the default style options + * @param defStyleAttr An attribute in the current theme that contains a + * reference to a style resource that supplies default values for + * the view. Can be 0 to not look for defaults. + * @param defStyleRes A resource identifier of a style resource that + * supplies default values for the view, used only if + * defStyleAttr is 0 or can not be found in the theme. Can be 0 + * to not look for defaults. */ - public SwitchPreference(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public SwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.SwitchPreference, defStyle, 0); + com.android.internal.R.styleable.SwitchPreference, defStyleAttr, defStyleRes); setSummaryOn(a.getString(com.android.internal.R.styleable.SwitchPreference_summaryOn)); setSummaryOff(a.getString(com.android.internal.R.styleable.SwitchPreference_summaryOff)); setSwitchTextOn(a.getString( @@ -83,6 +89,19 @@ public class SwitchPreference extends TwoStatePreference { * * @param context The Context that will style this preference * @param attrs Style attributes that differ from the default + * @param defStyleAttr An attribute in the current theme that contains a + * reference to a style resource that supplies default values for + * the view. Can be 0 to not look for defaults. + */ + public SwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + /** + * Construct a new SwitchPreference with the given style options. + * + * @param context The Context that will style this preference + * @param attrs Style attributes that differ from the default */ public SwitchPreference(Context context, AttributeSet attrs) { this(context, attrs, com.android.internal.R.attr.switchPreferenceStyle); diff --git a/core/java/android/preference/TwoStatePreference.java b/core/java/android/preference/TwoStatePreference.java index af83953..6f8be1f 100644 --- a/core/java/android/preference/TwoStatePreference.java +++ b/core/java/android/preference/TwoStatePreference.java @@ -42,9 +42,13 @@ public abstract class TwoStatePreference extends Preference { private boolean mSendClickAccessibilityEvent; private boolean mDisableDependentsState; + public TwoStatePreference( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } - public TwoStatePreference(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public TwoStatePreference(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); } public TwoStatePreference(Context context, AttributeSet attrs) { diff --git a/core/java/android/preference/VolumePreference.java b/core/java/android/preference/VolumePreference.java index dc683a6..29f2545 100644 --- a/core/java/android/preference/VolumePreference.java +++ b/core/java/android/preference/VolumePreference.java @@ -51,15 +51,24 @@ public class VolumePreference extends SeekBarDialogPreference implements /** May be null if the dialog isn't visible. */ private SeekBarVolumizer mSeekBarVolumizer; - public VolumePreference(Context context, AttributeSet attrs) { - super(context, attrs); + public VolumePreference( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.VolumePreference, 0, 0); + final TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.VolumePreference, defStyleAttr, defStyleRes); mStreamType = a.getInt(android.R.styleable.VolumePreference_streamType, 0); a.recycle(); } + public VolumePreference(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public VolumePreference(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + public void setStreamType(int streamType) { mStreamType = streamType; } diff --git a/core/java/android/print/PrintManager.java b/core/java/android/print/PrintManager.java index d1bb8fd..e4f73cb 100644 --- a/core/java/android/print/PrintManager.java +++ b/core/java/android/print/PrintManager.java @@ -359,6 +359,17 @@ public final class PrintManager { * selected the hinted options in the print dialog, given the current printer * supports them. * </p> + * <p> + * <strong>Note:</strong> Calling this method will bring the print dialog and + * the system will connect to the provided {@link PrintDocumentAdapter}. If a + * configuration change occurs that you application does not handle, for example + * a rotation change, the system will drop the connection to the adapter as the + * activity has to be recreated and the old adapter may be invalid in this context, + * hence a new adapter instance is required. As a consequence, if your activity + * does not handle configuration changes (default behavior), you have to save the + * state that you were printing and call this method again when your activity + * is recreated. + * </p> * * @param printJobName A name for the new print job which is shown to the user. * @param documentAdapter An adapter that emits the document to print. diff --git a/core/java/android/provider/CallLog.java b/core/java/android/provider/CallLog.java index a6f23a8..3b0d7ff 100644 --- a/core/java/android/provider/CallLog.java +++ b/core/java/android/provider/CallLog.java @@ -86,10 +86,8 @@ public class CallLog { public static final String ALLOW_VOICEMAILS_PARAM_KEY = "allow_voicemails"; /** - * Content uri with {@link #ALLOW_VOICEMAILS_PARAM_KEY} set. This can directly be used to - * access call log entries that includes voicemail records. - * - * @hide + * Content uri used to access call log entries, including voicemail records. You must have + * the READ_CALL_LOG and WRITE_CALL_LOG permissions to read and write to the call log. */ public static final Uri CONTENT_URI_WITH_VOICEMAIL = CONTENT_URI.buildUpon() .appendQueryParameter(ALLOW_VOICEMAILS_PARAM_KEY, "true") @@ -124,10 +122,7 @@ public class CallLog { public static final int OUTGOING_TYPE = 2; /** Call log type for missed calls. */ public static final int MISSED_TYPE = 3; - /** - * Call log type for voicemails. - * @hide - */ + /** Call log type for voicemails. */ public static final int VOICEMAIL_TYPE = 4; /** @@ -168,8 +163,6 @@ public class CallLog { * <P> * Type: TEXT * </P> - * - * @hide */ public static final String COUNTRY_ISO = "countryiso"; @@ -220,7 +213,6 @@ public class CallLog { /** * URI of the voicemail entry. Populated only for {@link #VOICEMAIL_TYPE}. * <P>Type: TEXT</P> - * @hide */ public static final String VOICEMAIL_URI = "voicemail_uri"; @@ -238,51 +230,48 @@ public class CallLog { * <p> * The string represents a city, state, or country associated with the number. * <P>Type: TEXT</P> - * @hide */ public static final String GEOCODED_LOCATION = "geocoded_location"; /** * The cached URI to look up the contact associated with the phone number, if it exists. - * This value is not guaranteed to be current, if the contact information - * associated with this number has changed. + * This value may not be current if the contact information associated with this number + * has changed. * <P>Type: TEXT</P> - * @hide */ public static final String CACHED_LOOKUP_URI = "lookup_uri"; /** * The cached phone number of the contact which matches this entry, if it exists. - * This value is not guaranteed to be current, if the contact information - * associated with this number has changed. + * This value may not be current if the contact information associated with this number + * has changed. * <P>Type: TEXT</P> - * @hide */ public static final String CACHED_MATCHED_NUMBER = "matched_number"; /** - * The cached normalized version of the phone number, if it exists. - * This value is not guaranteed to be current, if the contact information - * associated with this number has changed. + * The cached normalized(E164) version of the phone number, if it exists. + * This value may not be current if the contact information associated with this number + * has changed. * <P>Type: TEXT</P> - * @hide */ public static final String CACHED_NORMALIZED_NUMBER = "normalized_number"; /** * The cached photo id of the picture associated with the phone number, if it exists. - * This value is not guaranteed to be current, if the contact information - * associated with this number has changed. + * This value may not be current if the contact information associated with this number + * has changed. * <P>Type: INTEGER (long)</P> - * @hide */ public static final String CACHED_PHOTO_ID = "photo_id"; /** - * The cached formatted phone number. - * This value is not guaranteed to be present. + * The cached phone number, formatted with formatting rules based on the country the + * user was in when the call was made or received. + * This value is not guaranteed to be present, and may not be current if the contact + * information associated with this number + * has changed. * <P>Type: TEXT</P> - * @hide */ public static final String CACHED_FORMATTED_NUMBER = "formatted_number"; diff --git a/core/java/android/provider/Contacts.java b/core/java/android/provider/Contacts.java index c7e3c08..9e2aacd 100644 --- a/core/java/android/provider/Contacts.java +++ b/core/java/android/provider/Contacts.java @@ -26,7 +26,6 @@ import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; -import android.os.Build; import android.text.TextUtils; import android.util.Log; import android.widget.ImageView; diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java index 0863368..11678a6 100644 --- a/core/java/android/provider/ContactsContract.java +++ b/core/java/android/provider/ContactsContract.java @@ -47,9 +47,6 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; /** * <p> @@ -167,8 +164,6 @@ public final class ContactsContract { * {@link Contacts#CONTENT_STREQUENT_FILTER_URI}, which requires the ContactsProvider to * return only phone-related results. For example, frequently contacted person list should * include persons contacted via phone (not email, sms, etc.) - * - * @hide */ public static final String STREQUENT_PHONE_ONLY = "strequent_phone_only"; @@ -193,8 +188,6 @@ public final class ContactsContract { * {@link CommonDataKinds.Email#CONTENT_URI}, and * {@link CommonDataKinds.StructuredPostal#CONTENT_URI}. * This enables a content provider to remove duplicate entries in results. - * - * @hide */ public static final String REMOVE_DUPLICATE_ENTRIES = "remove_duplicate_entries"; @@ -251,30 +244,21 @@ public final class ContactsContract { public static final String KEY_AUTHORIZED_URI = "authorized_uri"; } - /** - * @hide - */ public static final class Preferences { /** * A key in the {@link android.provider.Settings android.provider.Settings} provider * that stores the preferred sorting order for contacts (by given name vs. by family name). - * - * @hide */ public static final String SORT_ORDER = "android.contacts.SORT_ORDER"; /** * The value for the SORT_ORDER key corresponding to sorting by given name first. - * - * @hide */ public static final int SORT_ORDER_PRIMARY = 1; /** * The value for the SORT_ORDER key corresponding to sorting by family name first. - * - * @hide */ public static final int SORT_ORDER_ALTERNATIVE = 2; @@ -282,22 +266,16 @@ public final class ContactsContract { * A key in the {@link android.provider.Settings android.provider.Settings} provider * that stores the preferred display order for contacts (given name first vs. family * name first). - * - * @hide */ public static final String DISPLAY_ORDER = "android.contacts.DISPLAY_ORDER"; /** * The value for the DISPLAY_ORDER key corresponding to showing the given name first. - * - * @hide */ public static final int DISPLAY_ORDER_PRIMARY = 1; /** * The value for the DISPLAY_ORDER key corresponding to showing the family name first. - * - * @hide */ public static final int DISPLAY_ORDER_ALTERNATIVE = 2; } @@ -827,10 +805,9 @@ public final class ContactsContract { public static final String STARRED = "starred"; /** - * The position at which the contact is pinned. If {@link PinnedPositions.UNPINNED}, + * The position at which the contact is pinned. If {@link PinnedPositions#UNPINNED}, * the contact is not pinned. Also see {@link PinnedPositions}. * <P>Type: INTEGER </P> - * @hide */ public static final String PINNED = "pinned"; @@ -924,6 +901,14 @@ public final class ContactsContract { public static final String PHOTO_THUMBNAIL_URI = "photo_thumb_uri"; /** + * Flag that reflects whether the contact exists inside the default directory. + * Ie, whether the contact is designed to only be visible outside search. + * + * @hide + */ + public static final String IN_DEFAULT_DIRECTORY = "in_default_directory"; + + /** * Flag that reflects the {@link Groups#GROUP_VISIBLE} state of any * {@link CommonDataKinds.GroupMembership} for this contact. */ @@ -1470,17 +1455,43 @@ public final class ContactsContract { * Base {@link Uri} for referencing multiple {@link Contacts} entry, * created by appending {@link #LOOKUP_KEY} using * {@link Uri#withAppendedPath(Uri, String)}. The lookup keys have to be - * encoded and joined with the colon (":") separator. The resulting string - * has to be encoded again. Provides - * {@link OpenableColumns} columns when queried, or returns the + * joined with the colon (":") separator, and the resulting string encoded. + * + * Provides {@link OpenableColumns} columns when queried, or returns the * referenced contact formatted as a vCard when opened through * {@link ContentResolver#openAssetFileDescriptor(Uri, String)}. * - * This is private API because we do not have a well-defined way to - * specify several entities yet. The format of this Uri might change in the future - * or the Uri might be completely removed. + * <p> + * Usage example: + * <dl> + * <dt>The following code snippet creates a multi-vcard URI that references all the + * contacts in a user's database.</dt> + * <dd> * - * @hide + * <pre> + * public Uri getAllContactsVcardUri() { + * Cursor cursor = getActivity().getContentResolver().query(Contacts.CONTENT_URI, + * new String[] {Contacts.LOOKUP_KEY}, null, null, null); + * if (cursor == null) { + * return null; + * } + * try { + * StringBuilder uriListBuilder = new StringBuilder(); + * int index = 0; + * while (cursor.moveToNext()) { + * if (index != 0) uriListBuilder.append(':'); + * uriListBuilder.append(cursor.getString(0)); + * index++; + * } + * return Uri.withAppendedPath(Contacts.CONTENT_MULTI_VCARD_URI, + * Uri.encode(uriListBuilder.toString())); + * } finally { + * cursor.close(); + * } + * } + * </pre> + * + * </p> */ public static final Uri CONTENT_MULTI_VCARD_URI = Uri.withAppendedPath(CONTENT_URI, "as_multi_vcard"); @@ -4794,11 +4805,11 @@ public final class ContactsContract { */ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/phone_lookup"; - /** - * Boolean parameter that is used to look up a SIP address. - * - * @hide - */ + /** + * If this boolean parameter is set to true, then the appended query is treated as a + * SIP address and the lookup will be performed against SIP addresses in the user's + * contacts. + */ public static final String QUERY_PARAMETER_SIP_ADDRESS = "sip"; } @@ -5307,8 +5318,6 @@ public final class ContactsContract { /** * The style used for combining given/middle/family name into a full name. * See {@link ContactsContract.FullNameStyle}. - * - * @hide */ public static final String FULL_NAME_STYLE = DATA10; @@ -6900,8 +6909,6 @@ public final class ContactsContract { * each column. For example the meaning for {@link Phone}'s type is different than * {@link SipAddress}'s. * </p> - * - * @hide */ public static final class Callable implements DataColumnsWithJoins, CommonColumns { /** @@ -7759,7 +7766,6 @@ public final class ContactsContract { * {@link PinnedPositions#STAR_WHEN_PINNING} to true to force all pinned and unpinned * contacts to be automatically starred and unstarred. * </p> - * @hide */ public static final class PinnedPositions { diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java index f69cad0..ae24968 100644 --- a/core/java/android/provider/MediaStore.java +++ b/core/java/android/provider/MediaStore.java @@ -169,6 +169,18 @@ public final class MediaStore { */ public static final String EXTRA_MEDIA_TITLE = "android.intent.extra.title"; /** + * The name of the Intent-extra used to define the genre. + */ + public static final String EXTRA_MEDIA_GENRE = "android.intent.extra.genre"; + /** + * The name of the Intent-extra used to define the playlist. + */ + public static final String EXTRA_MEDIA_PLAYLIST = "android.intent.extra.playlist"; + /** + * The name of the Intent-extra used to define the radio channel. + */ + public static final String EXTRA_MEDIA_RADIO_CHANNEL = "android.intent.extra.radio_channel"; + /** * The name of the Intent-extra used to define the search focus. The search focus * indicates whether the search should be for things related to the artist, album * or song that is identified by the other extras. @@ -1389,6 +1401,11 @@ public final class MediaStore { public static final String CONTENT_TYPE = "vnd.android.cursor.dir/audio"; /** + * The MIME type for an audio track. + */ + public static final String ENTRY_CONTENT_TYPE = "vnd.android.cursor.item/audio"; + + /** * The default sort order for this table */ public static final String DEFAULT_SORT_ORDER = TITLE_KEY; @@ -1859,6 +1876,13 @@ public final class MediaStore { */ public static final String DEFAULT_SORT_ORDER = ALBUM_KEY; } + + public static final class Radio { + /** + * The MIME type for entries in this table. + */ + public static final String ENTRY_CONTENT_TYPE = "vnd.android.cursor.item/radio"; + } } public static final class Video { diff --git a/core/java/android/provider/SearchIndexableData.java b/core/java/android/provider/SearchIndexableData.java new file mode 100644 index 0000000..60bcc40 --- /dev/null +++ b/core/java/android/provider/SearchIndexableData.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2014 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.provider; + +import android.content.Context; + +import java.util.Locale; + +/** + * The Indexable data for Search. + * + * This abstract class defines the common parts for all search indexable data. + * + * @hide + */ +public abstract class SearchIndexableData { + + /** + * The context for the data. Will usually allow retrieving some resources. + * + * @see Context + */ + public Context context; + + /** + * The locale for the data + */ + public Locale locale; + + /** + * Tells if the data will be included into the search results. This is application specific. + */ + public boolean enabled; + + /** + * The rank for the data. This is application specific. + */ + public int rank; + + /** + * The key for the data. This is application specific. Should be unique per data as the data + * should be able to be retrieved by the key. + */ + public String key; + + /** + * The class name associated with the data. Generally this is a Fragment class name for + * referring where the data is coming from and for launching the associated Fragment for + * displaying the data. This is used only when the data is provided "locally". + * + * If the data is provided "externally", the relevant information come from the + * {@link SearchIndexableData#intentAction} and {@link SearchIndexableData#intentTargetPackage} + * and {@link SearchIndexableData#intentTargetClass}. + * + * @see SearchIndexableData#intentAction + * @see SearchIndexableData#intentTargetPackage + * @see SearchIndexableData#intentTargetClass + */ + public String className; + + /** + * The package name for retrieving the icon associated with the data. + * + * @see SearchIndexableData#iconResId + */ + public String packageName; + + /** + * The icon resource ID associated with the data. + * + * @see SearchIndexableData#packageName + */ + public int iconResId; + + /** + * The Intent action associated with the data. This is used when the + * {@link SearchIndexableData#className} is not relevant. + * + * @see SearchIndexableData#intentTargetPackage + * @see SearchIndexableData#intentTargetClass + */ + public String intentAction; + + /** + * The Intent target package associated with the data. + * + * @see SearchIndexableData#intentAction + * @see SearchIndexableData#intentTargetClass + */ + public String intentTargetPackage; + + /** + * The Intent target class associated with the data. + * + * @see SearchIndexableData#intentAction + * @see SearchIndexableData#intentTargetPackage + */ + public String intentTargetClass; + + /** + * Default constructor. + */ + public SearchIndexableData() { + locale = Locale.getDefault(); + enabled = true; + } + + /** + * Constructor with a {@link Context}. + * + * @param ctx the Context + */ + public SearchIndexableData(Context ctx) { + this(); + context = ctx; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("SearchIndexableData[context: "); + sb.append(context); + sb.append(", "); + sb.append("locale: "); + sb.append(locale); + sb.append(", "); + sb.append("enabled: "); + sb.append(enabled); + sb.append(", "); + sb.append("rank: "); + sb.append(rank); + sb.append(", "); + sb.append("key: "); + sb.append(key); + sb.append(", "); + sb.append("className: "); + sb.append(className); + sb.append(", "); + sb.append("packageName: "); + sb.append(packageName); + sb.append(", "); + sb.append("iconResId: "); + sb.append(iconResId); + sb.append(", "); + sb.append("intentAction: "); + sb.append(intentAction); + sb.append(", "); + sb.append("intentTargetPackage: "); + sb.append(intentTargetPackage); + sb.append(", "); + sb.append("intentTargetClass: "); + sb.append(intentTargetClass); + sb.append("]"); + + return sb.toString(); + } +} diff --git a/core/java/android/provider/SearchIndexableResource.java b/core/java/android/provider/SearchIndexableResource.java new file mode 100644 index 0000000..c807df2 --- /dev/null +++ b/core/java/android/provider/SearchIndexableResource.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2014 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.provider; + +import android.content.Context; + +/** + * Search Indexable Resource. + * + * This class wraps a set of reference information representing data that can be indexed from a + * resource which would typically be a {@link android.preference.PreferenceScreen}. + * + * xmlResId: the resource ID of a {@link android.preference.PreferenceScreen} XML file. + * + * @see SearchIndexableData + * @see android.preference.PreferenceScreen + * + * @hide + */ +public class SearchIndexableResource extends SearchIndexableData { + + /** + * Resource ID of the associated {@link android.preference.PreferenceScreen} XML file. + */ + public int xmlResId; + + /** + * Constructor. + * + * @param rank the rank of the data. + * @param xmlResId the resource ID of a {@link android.preference.PreferenceScreen} XML file. + * @param className the class name associated with the data (generally a + * {@link android.app.Fragment}). + * @param iconResId the resource ID associated with the data. + */ + public SearchIndexableResource(int rank, int xmlResId, String className, int iconResId) { + super(); + this.rank = rank; + this.xmlResId = xmlResId; + this.className = className; + this.iconResId = iconResId; + } + + /** + * Constructor. + * + * @param context the Context associated with the data. + */ + public SearchIndexableResource(Context context) { + super(context); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("SearchIndexableResource["); + sb.append(super.toString()); + sb.append(", "); + sb.append("xmlResId: "); + sb.append(xmlResId); + sb.append("]"); + + return sb.toString(); + } +}
\ No newline at end of file diff --git a/core/java/android/provider/SearchIndexablesContract.java b/core/java/android/provider/SearchIndexablesContract.java new file mode 100644 index 0000000..a8b4cfb --- /dev/null +++ b/core/java/android/provider/SearchIndexablesContract.java @@ -0,0 +1,265 @@ +/* + * Copyright (C) 2014 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.provider; + +import android.content.ContentResolver; + +/** + * Describe the contract for an Indexable data. + * + * @hide + */ +public class SearchIndexablesContract { + + /** + * Intent action used to identify {@link SearchIndexablesProvider} + * instances. This is used in the {@code <intent-filter>} of a {@code <provider>}. + */ + public static final String PROVIDER_INTERFACE = + "android.content.action.SEARCH_INDEXABLES_PROVIDER"; + + private static final String SETTINGS = "settings"; + + /** + * Indexable reference names. + */ + public static final String INDEXABLES_XML_RES = "indexables_xml_res"; + + /** + * ContentProvider path for indexable xml resources. + */ + public static final String INDEXABLES_XML_RES_PATH = SETTINGS + "/" + INDEXABLES_XML_RES; + + /** + * Indexable raw data names. + */ + public static final String INDEXABLES_RAW = "indexables_raw"; + + /** + * ContentProvider path for indexable raw data. + */ + public static final String INDEXABLES_RAW_PATH = SETTINGS + "/" + INDEXABLES_RAW; + + /** + * Non indexable data keys. + */ + public static final String NON_INDEXABLES_KEYS = "non_indexables_key"; + + /** + * ContentProvider path for non indexable data keys. + */ + public static final String NON_INDEXABLES_KEYS_PATH = SETTINGS + "/" + NON_INDEXABLES_KEYS; + + /** + * Indexable xml resources colums. + */ + public static final String[] INDEXABLES_XML_RES_COLUMNS = new String[] { + XmlResource.COLUMN_RANK, // 0 + XmlResource.COLUMN_XML_RESID, // 1 + XmlResource.COLUMN_CLASS_NAME, // 2 + XmlResource.COLUMN_ICON_RESID, // 3 + XmlResource.COLUMN_INTENT_ACTION, // 4 + XmlResource.COLUMN_INTENT_TARGET_PACKAGE, // 5 + XmlResource.COLUMN_INTENT_TARGET_CLASS // 6 + }; + + /** + * Indexable xml resources colums indices. + */ + public static final int COLUMN_INDEX_XML_RES_RANK = 0; + public static final int COLUMN_INDEX_XML_RES_RESID = 1; + public static final int COLUMN_INDEX_XML_RES_CLASS_NAME = 2; + public static final int COLUMN_INDEX_XML_RES_ICON_RESID = 3; + public static final int COLUMN_INDEX_XML_RES_INTENT_ACTION = 4; + public static final int COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE = 5; + public static final int COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS = 6; + + /** + * Indexable raw data colums. + */ + public static final String[] INDEXABLES_RAW_COLUMNS = new String[] { + RawData.COLUMN_RANK, // 0 + RawData.COLUMN_TITLE, // 1 + RawData.COLUMN_SUMMARY_ON, // 2 + RawData.COLUMN_SUMMARY_OFF, // 3 + RawData.COLUMN_ENTRIES, // 4 + RawData.COLUMN_KEYWORDS, // 5 + RawData.COLUMN_SCREEN_TITLE, // 6 + RawData.COLUMN_CLASS_NAME, // 7 + RawData.COLUMN_ICON_RESID, // 8 + RawData.COLUMN_INTENT_ACTION, // 9 + RawData.COLUMN_INTENT_TARGET_PACKAGE, // 10 + RawData.COLUMN_INTENT_TARGET_CLASS, // 11 + RawData.COLUMN_KEY, // 12 + }; + + /** + * Indexable raw data colums indices. + */ + public static final int COLUMN_INDEX_RAW_RANK = 0; + public static final int COLUMN_INDEX_RAW_TITLE = 1; + public static final int COLUMN_INDEX_RAW_SUMMARY_ON = 2; + public static final int COLUMN_INDEX_RAW_SUMMARY_OFF = 3; + public static final int COLUMN_INDEX_RAW_ENTRIES = 4; + public static final int COLUMN_INDEX_RAW_KEYWORDS = 5; + public static final int COLUMN_INDEX_RAW_SCREEN_TITLE = 6; + public static final int COLUMN_INDEX_RAW_CLASS_NAME = 7; + public static final int COLUMN_INDEX_RAW_ICON_RESID = 8; + public static final int COLUMN_INDEX_RAW_INTENT_ACTION = 9; + public static final int COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE = 10; + public static final int COLUMN_INDEX_RAW_INTENT_TARGET_CLASS = 11; + public static final int COLUMN_INDEX_RAW_KEY = 12; + + /** + * Indexable raw data colums. + */ + public static final String[] NON_INDEXABLES_KEYS_COLUMNS = new String[] { + NonIndexableKey.COLUMN_KEY_VALUE // 0 + }; + + /** + * Non indexable data keys colums indices. + */ + public static final int COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE = 0; + + /** + * Constants related to a {@link SearchIndexableResource}. + * + * This is a description of + */ + public static final class XmlResource extends BaseColumns { + private XmlResource() { + } + + public static final String MIME_TYPE = ContentResolver.CURSOR_DIR_BASE_TYPE + + "/" + INDEXABLES_XML_RES; + + /** + * XML resource ID for the {@link android.preference.PreferenceScreen} to load and index. + */ + public static final String COLUMN_XML_RESID = "xmlResId"; + } + + /** + * Constants related to a {@link SearchIndexableData}. + * + * This is the raw data that is stored into an Index. This is related to + * {@link android.preference.Preference} and its attributes like + * {@link android.preference.Preference#getTitle()}, + * {@link android.preference.Preference#getSummary()}, etc. + * + */ + public static final class RawData extends BaseColumns { + private RawData() { + } + + public static final String MIME_TYPE = ContentResolver.CURSOR_DIR_BASE_TYPE + + "/" + INDEXABLES_RAW; + + /** + * Title's raw data. + */ + public static final String COLUMN_TITLE = "title"; + + /** + * Summary's raw data when the data is "ON". + */ + public static final String COLUMN_SUMMARY_ON = "summaryOn"; + + /** + * Summary's raw data when the data is "OFF". + */ + public static final String COLUMN_SUMMARY_OFF = "summaryOff"; + + /** + * Entries associated with the raw data (when the data can have several values). + */ + public static final String COLUMN_ENTRIES = "entries"; + + /** + * Keywords' raw data. + */ + public static final String COLUMN_KEYWORDS = "keywords"; + + /** + * Fragment or Activity title associated with the raw data. + */ + public static final String COLUMN_SCREEN_TITLE = "screenTitle"; + + /** + * Key associated with the raw data. The key needs to be unique. + */ + public static final String COLUMN_KEY = "key"; + } + + /** + * Constants related to a {@link SearchIndexableResource} and {@link SearchIndexableData}. + * + * This is a description of a data (thru its unique key) that cannot be indexed. + */ + public static final class NonIndexableKey extends BaseColumns { + private NonIndexableKey() { + } + + public static final String MIME_TYPE = ContentResolver.CURSOR_DIR_BASE_TYPE + + "/" + NON_INDEXABLES_KEYS; + + /** + * Key for the non indexable data. + */ + public static final String COLUMN_KEY_VALUE = "key"; + } + + /** + * The base columns. + */ + private static class BaseColumns { + private BaseColumns() { + } + + /** + * Rank of the data. This is an integer used for ranking the search results. This is + * application specific. + */ + public static final String COLUMN_RANK = "rank"; + + /** + * Class name associated with the data (usually a Fragment class name). + */ + public static final String COLUMN_CLASS_NAME = "className"; + + /** + * Icon resource ID for the data. + */ + public static final String COLUMN_ICON_RESID = "iconResId"; + + /** + * Intent action associated with the data. + */ + public static final String COLUMN_INTENT_ACTION = "intentAction"; + + /** + * Intent target package associated with the data. + */ + public static final String COLUMN_INTENT_TARGET_PACKAGE = "intentTargetPackage"; + + /** + * Intent target class associated with the data. + */ + public static final String COLUMN_INTENT_TARGET_CLASS = "intentTargetClass"; + } +} diff --git a/core/java/android/provider/SearchIndexablesProvider.java b/core/java/android/provider/SearchIndexablesProvider.java new file mode 100644 index 0000000..9c8f6d0 --- /dev/null +++ b/core/java/android/provider/SearchIndexablesProvider.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2014 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.provider; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.Context; +import android.content.UriMatcher; +import android.content.pm.ProviderInfo; +import android.database.Cursor; +import android.net.Uri; + +/** + * Base class for a search indexable provider. Such provider offers data to be indexed either + * as a reference to an XML file (like a {@link android.preference.PreferenceScreen}) or either + * as some raw data. + * + * @see SearchIndexableResource + * @see SearchIndexableData + * @see SearchIndexablesContract + * + * To create a search indexables provider, extend this class, then implement the abstract methods, + * and add it to your manifest like this: + * + * <pre class="prettyprint"><manifest> + * ... + * <application> + * ... + * <provider + * android:name="com.example.MyIndexablesProvider" + * android:authorities="com.example.myindexablesprovider" + * android:exported="true" + * android:grantUriPermissions="true" + * android:permission="android.permission.READ_SEARCH_INDEXABLES" + * <intent-filter> + * <action android:name="android.content.action.SEARCH_INDEXABLES_PROVIDER" /> + * </intent-filter> + * </provider> + * ... + * </application> + *</manifest></pre> + * <p> + * When defining your provider, you must protect it with + * {@link android.Manifest.permission#READ_SEARCH_INDEXABLES}, which is a permission only the system + * can obtain. + * </p> + * + * @hide + */ +public abstract class SearchIndexablesProvider extends ContentProvider { + private static final String TAG = "IndexablesProvider"; + + private String mAuthority; + private UriMatcher mMatcher; + + private static final int MATCH_RES_CODE = 1; + private static final int MATCH_RAW_CODE = 2; + private static final int MATCH_NON_INDEXABLE_KEYS_CODE = 3; + + /** + * Implementation is provided by the parent class. + */ + @Override + public void attachInfo(Context context, ProviderInfo info) { + mAuthority = info.authority; + + mMatcher = new UriMatcher(UriMatcher.NO_MATCH); + mMatcher.addURI(mAuthority, SearchIndexablesContract.INDEXABLES_XML_RES_PATH, + MATCH_RES_CODE); + mMatcher.addURI(mAuthority, SearchIndexablesContract.INDEXABLES_RAW_PATH, + MATCH_RAW_CODE); + mMatcher.addURI(mAuthority, SearchIndexablesContract.NON_INDEXABLES_KEYS_PATH, + MATCH_NON_INDEXABLE_KEYS_CODE); + + // Sanity check our setup + if (!info.exported) { + throw new SecurityException("Provider must be exported"); + } + if (!info.grantUriPermissions) { + throw new SecurityException("Provider must grantUriPermissions"); + } + if (!android.Manifest.permission.READ_SEARCH_INDEXABLES.equals(info.readPermission)) { + throw new SecurityException("Provider must be protected by READ_SEARCH_INDEXABLES"); + } + + super.attachInfo(context, info); + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + switch (mMatcher.match(uri)) { + case MATCH_RES_CODE: + return queryXmlResources(null); + case MATCH_RAW_CODE: + return queryRawData(null); + case MATCH_NON_INDEXABLE_KEYS_CODE: + return queryNonIndexableKeys(null); + default: + throw new UnsupportedOperationException("Unknown Uri " + uri); + } + } + + /** + * Returns all {@link android.provider.SearchIndexablesContract.XmlResource}. + * + * Those are Xml resource IDs to some {@link android.preference.PreferenceScreen}. + * + * @param projection list of {@link android.provider.SearchIndexablesContract.XmlResource} + * columns to put into the cursor. If {@code null} all supported columns + * should be included. + */ + public abstract Cursor queryXmlResources(String[] projection); + + /** + * Returns all {@link android.provider.SearchIndexablesContract.RawData}. + * + * Those are the raw indexable data. + * + * @param projection list of {@link android.provider.SearchIndexablesContract.RawData} columns + * to put into the cursor. If {@code null} all supported columns should be + * included. + */ + public abstract Cursor queryRawData(String[] projection); + + /** + * Returns all {@link android.provider.SearchIndexablesContract.NonIndexableKey}. + * + * Those are the non indexable data keys. + * + * @param projection list of {@link android.provider.SearchIndexablesContract.NonIndexableKey} + * columns to put into the cursor. If {@code null} all supported columns + * should be included. + */ + public abstract Cursor queryNonIndexableKeys(String[] projection); + + @Override + public String getType(Uri uri) { + switch (mMatcher.match(uri)) { + case MATCH_RES_CODE: + return SearchIndexablesContract.XmlResource.MIME_TYPE; + case MATCH_RAW_CODE: + return SearchIndexablesContract.RawData.MIME_TYPE; + case MATCH_NON_INDEXABLE_KEYS_CODE: + return SearchIndexablesContract.NonIndexableKey.MIME_TYPE; + default: + throw new IllegalArgumentException("Unknown URI " + uri); + } + } + + /** + * Implementation is provided by the parent class. Throws by default, and cannot be overriden. + */ + @Override + public final Uri insert(Uri uri, ContentValues values) { + throw new UnsupportedOperationException("Insert not supported"); + } + + /** + * Implementation is provided by the parent class. Throws by default, and cannot be overriden. + */ + @Override + public final int delete(Uri uri, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException("Delete not supported"); + } + + /** + * Implementation is provided by the parent class. Throws by default, and cannot be overriden. + */ + @Override + public final int update( + Uri uri, ContentValues values, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException("Update not supported"); + } +} diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 3810eb3..7062933 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -749,6 +749,14 @@ public final class Settings { public static final String ACTION_PRINT_SETTINGS = "android.settings.ACTION_PRINT_SETTINGS"; + /** + * Activity Action: Show Zen Mode configuration settings. + * + * @hide + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_ZEN_MODE_SETTINGS = "android.settings.ZEN_MODE_SETTINGS"; + // End of Intent actions for Settings /** @@ -3352,21 +3360,29 @@ public final class Settings { public static final String INSTALL_NON_MARKET_APPS = Global.INSTALL_NON_MARKET_APPS; /** - * Comma-separated list of location providers that activities may access. + * Comma-separated list of location providers that activities may access. Do not rely on + * this value being present in settings.db or on ContentObserver notifications on the + * corresponding Uri. * - * @deprecated use {@link #LOCATION_MODE} + * @deprecated use {@link #LOCATION_MODE} and + * {@link LocationManager#MODE_CHANGED_ACTION} (or + * {@link LocationManager#PROVIDERS_CHANGED_ACTION}) */ @Deprecated public static final String LOCATION_PROVIDERS_ALLOWED = "location_providers_allowed"; /** * The degree of location access enabled by the user. - * <p/> + * <p> * When used with {@link #putInt(ContentResolver, String, int)}, must be one of {@link * #LOCATION_MODE_HIGH_ACCURACY}, {@link #LOCATION_MODE_SENSORS_ONLY}, {@link * #LOCATION_MODE_BATTERY_SAVING}, or {@link #LOCATION_MODE_OFF}. When used with {@link * #getInt(ContentResolver, String)}, the caller must gracefully handle additional location * modes that might be added in the future. + * <p> + * Note: do not rely on this value being present in settings.db or on ContentObserver + * notifications for the corresponding Uri. Use {@link LocationManager#MODE_CHANGED_ACTION} + * to receive changes in this value. */ public static final String LOCATION_MODE = "location_mode"; @@ -3407,6 +3423,11 @@ public final class Settings { public static final String LOCK_PATTERN_VISIBLE = "lock_pattern_visible_pattern"; /** + * Whether the NFC unlock feature is enabled (0 = false, 1 = true) + */ + public static final String NFC_UNLOCK_ENABLED = "nfc_unlock_enabled"; + + /** * Whether lock pattern will vibrate as user enters (0 = false, 1 = * true) * @@ -3463,6 +3484,14 @@ public final class Settings { "lock_screen_owner_info_enabled"; /** + * When set by a user, allows notifications to be shown atop a securely locked screen + * in their full "private" form (same as when the device is unlocked). + * @hide + */ + public static final String LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS = + "lock_screen_allow_private_notifications"; + + /** * The Logging ID (a unique 64-bit value) as a hex string. * Used as a pseudonymous identifier for logging. * @deprecated This identifier is poorly initialized and has @@ -3741,6 +3770,16 @@ public final class Settings { "accessibility_captioning_edge_color"; /** + * Integer property that specifes the window color for captions as a + * packed 32-bit color. + * + * @see android.graphics.Color#argb + * @hide + */ + public static final String ACCESSIBILITY_CAPTIONING_WINDOW_COLOR = + "accessibility_captioning_window_color"; + + /** * String property that specifies the typeface for captions, one of: * <ul> * <li>DEFAULT @@ -3764,6 +3803,97 @@ public final class Settings { "accessibility_captioning_font_scale"; /** + * Setting that specifies whether the quick setting tile for display + * color inversion is enabled. + * + * @hide + */ + public static final String ACCESSIBILITY_DISPLAY_INVERSION_QUICK_SETTING_ENABLED = + "accessibility_display_inversion_quick_setting_enabled"; + + /** + * Setting that specifies whether display color inversion is enabled. + * + * @hide + */ + public static final String ACCESSIBILITY_DISPLAY_INVERSION_ENABLED = + "accessibility_display_inversion_enabled"; + + /** + * Integer property that specifies the type of color inversion to + * perform. Valid values are defined in AccessibilityManager. + * + * @hide + */ + public static final String ACCESSIBILITY_DISPLAY_INVERSION = + "accessibility_display_inversion"; + + /** + * Setting that specifies whether the quick setting tile for display + * color space adjustment is enabled. + * + * @hide + */ + public static final String ACCESSIBILITY_DISPLAY_DALTONIZER_QUICK_SETTING_ENABLED = + "accessibility_display_daltonizer_quick_setting_enabled"; + + /** + * Setting that specifies whether display color space adjustment is + * enabled. + * + * @hide + */ + public static final String ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED = + "accessibility_display_daltonizer_enabled"; + + /** + * Integer property that specifies the type of color space adjustment to + * perform. Valid values are defined in AccessibilityManager. + * + * @hide + */ + public static final String ACCESSIBILITY_DISPLAY_DALTONIZER = + "accessibility_display_daltonizer"; + + /** + * Setting that specifies whether the quick setting tile for display + * contrast enhancement is enabled. + * + * @hide + */ + public static final String ACCESSIBILITY_DISPLAY_CONTRAST_QUICK_SETTING_ENABLED = + "accessibility_display_contrast_quick_setting_enabled"; + + /** + * Setting that specifies whether display contrast enhancement is + * enabled. + * + * @hide + */ + public static final String ACCESSIBILITY_DISPLAY_CONTRAST_ENABLED = + "accessibility_display_contrast_enabled"; + + /** + * Floating point property that specifies display contrast adjustment. + * Valid range is [0, ...] where 0 is gray, 1 is normal, and higher + * values indicate enhanced contrast. + * + * @hide + */ + public static final String ACCESSIBILITY_DISPLAY_CONTRAST = + "accessibility_display_contrast"; + + /** + * Floating point property that specifies display brightness adjustment. + * Valid range is [-1, 1] where -1 is black, 0 is default, and 1 is + * white. + * + * @hide + */ + public static final String ACCESSIBILITY_DISPLAY_BRIGHTNESS = + "accessibility_display_brightness"; + + /** * The timout for considering a press to be a long press in milliseconds. * @hide */ @@ -5941,6 +6071,59 @@ public final class Settings { public static final String LOW_BATTERY_SOUND_TIMEOUT = "low_battery_sound_timeout"; /** + * Milliseconds to wait before bouncing Wi-Fi after settings is restored. Note that after + * the caller is done with this, they should call {@link ContentResolver#delete(Uri)} to + * clean up any value that they may have written. + * + * @hide + */ + public static final String WIFI_BOUNCE_DELAY_OVERRIDE_MS = "wifi_bounce_delay_override_ms"; + + /** + * Defines global runtime overrides to window policy. + * + * See {@link com.android.internal.policy.impl.PolicyControl} for value format. + * + * @hide + */ + public static final String POLICY_CONTROL = "policy_control"; + + + /** + * This preference enables notification display even over a securely + * locked screen. + * @hide + */ + public static final String LOCK_SCREEN_SHOW_NOTIFICATIONS = + "lock_screen_show_notifications"; + + /** + * Defines global zen mode. ZEN_MODE_OFF or ZEN_MODE_ON. + * + * @hide + */ + public static final String ZEN_MODE = "zen_mode"; + + /** @hide */ public static final int ZEN_MODE_OFF = 0; + /** @hide */ public static final int ZEN_MODE_ON = 1; + + /** @hide */ public static String zenModeToString(int mode) { + if (mode == ZEN_MODE_OFF) return "ZEN_MODE_OFF"; + return "ZEN_MODE_ON"; + } + + /** + * Defines global heads up toggle. One of HEADS_UP_OFF, HEADS_UP_ON. + * + * @hide + */ + public static final String HEADS_UP_NOTIFICATIONS_ENABLED = + "heads_up_notifications_enabled"; + + /** @hide */ public static final int HEADS_UP_OFF = 0; + /** @hide */ public static final int HEADS_UP_ON = 1; + + /** * Settings to backup. This is here so that it's in the same place as the settings * keys and easy to update. * diff --git a/core/java/android/provider/TvContract.java b/core/java/android/provider/TvContract.java new file mode 100644 index 0000000..233e0ca --- /dev/null +++ b/core/java/android/provider/TvContract.java @@ -0,0 +1,449 @@ +/* + * Copyright (C) 2014 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.provider; + +import android.content.ContentUris; +import android.net.Uri; + +/** + * <p> + * The contract between the TV provider and applications. Contains definitions for the supported + * URIs and columns. + * </p> + * <h3>Overview</h3> + * <p> + * TvContract defines a basic database of TV content metadata such as channel and program + * information. The information is stored in {@link Channels} and {@link Programs} tables. + * </p> + * <ul> + * <li>A row in the {@link Channels} table represents information about a TV channel. The data + * format can vary greatly from standard to standard or according to service provider, thus + * the columns here are mostly comprised of basic entities that are usually seen to users + * regardless of standard such as channel number and name.</li> + * <li>A row in the {@link Programs} table represents a set of data describing a TV program such + * as program title and start time.</li> + * </ul> + */ +public final class TvContract { + /** The authority for the TV provider. */ + public static final String AUTHORITY = "com.android.tv"; + + /** + * Builds a URI that points to a specific channel. + * + * @param channelId The ID of the channel to point to. + */ + public static final Uri buildChannelUri(long channelId) { + return ContentUris.withAppendedId(Channels.CONTENT_URI, channelId); + } + + /** + * Builds a URI that points to a specific program. + * + * @param programId The ID of the program to point to. + */ + public static final Uri buildProgramUri(long programId) { + return ContentUris.withAppendedId(Programs.CONTENT_URI, programId); + } + + /** + * Builds a URI that points to a specific program the user watched. + * + * @param watchedProgramId The ID of the watched program to point to. + * @hide + */ + public static final Uri buildWatchedProgramUri(long watchedProgramId) { + return ContentUris.withAppendedId(WatchedPrograms.CONTENT_URI, watchedProgramId); + } + + private TvContract() {} + + /** + * Common base for the tables of TV channels/programs. + */ + public interface BaseTvColumns extends BaseColumns { + /** + * The name of the package that owns a row in each table. + * <p> + * The TV provider fills it in with the name of the package that provides the initial data + * of that row. If the package is later uninstalled, the rows it owns are automatically + * removed from the tables. + * </p><p> + * Type: TEXT + * </p> + */ + public static final String PACKAGE_NAME = "package_name"; + } + + /** Column definitions for the TV channels table. */ + public static final class Channels implements BaseTvColumns { + + /** The content:// style URI for this table. */ + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/channel"); + + /** The MIME type of a directory of TV channels. */ + public static final String CONTENT_TYPE = + "vnd.android.cursor.dir/vnd.com.android.tv.channels"; + + /** The MIME type of a single TV channel. */ + public static final String CONTENT_ITEM_TYPE = + "vnd.android.cursor.item/vnd.com.android.tv.channels"; + + /** A generic channel type. */ + public static final int TYPE_OTHER = 0x0; + + /** The special channel type used for pass-through inputs such as HDMI. */ + public static final int TYPE_PASSTHROUGH = 0x00010000; + + /** The channel type for DVB-T (terrestrial). */ + public static final int TYPE_DVB_T = 0x00020000; + + /** The channel type for DVB-T2 (terrestrial). */ + public static final int TYPE_DVB_T2 = 0x00020001; + + /** The channel type for DVB-S (satellite). */ + public static final int TYPE_DVB_S = 0x00020100; + + /** The channel type for DVB-S2 (satellite). */ + public static final int TYPE_DVB_S2 = 0x00020101; + + /** The channel type for DVB-C (cable). */ + public static final int TYPE_DVB_C = 0x00020200; + + /** The channel type for DVB-C2 (cable). */ + public static final int TYPE_DVB_C2 = 0x00020201; + + /** The channel type for DVB-H (handheld). */ + public static final int TYPE_DVB_H = 0x00020300; + + /** The channel type for DVB-SH (satellite). */ + public static final int TYPE_DVB_SH = 0x00020400; + + /** The channel type for ATSC (terrestrial/cable). */ + public static final int TYPE_ATSC = 0x00030000; + + /** The channel type for ATSC 2.0. */ + public static final int TYPE_ATSC_2_0 = 0x00030001; + + /** The channel type for ATSC-M/H (mobile/handheld). */ + public static final int TYPE_ATSC_M_H = 0x00030100; + + /** The channel type for ISDB-T (terrestrial). */ + public static final int TYPE_ISDB_T = 0x00040000; + + /** The channel type for ISDB-Tb (Brazil). */ + public static final int TYPE_ISDB_TB = 0x00040100; + + /** The channel type for ISDB-S (satellite). */ + public static final int TYPE_ISDB_S = 0x00040200; + + /** The channel type for ISDB-C (cable). */ + public static final int TYPE_ISDB_C = 0x00040300; + + /** The channel type for 1seg (handheld). */ + public static final int TYPE_1SEG = 0x00040400; + + /** The channel type for DTMB (terrestrial). */ + public static final int TYPE_DTMB = 0x00050000; + + /** The channel type for CMMB (handheld). */ + public static final int TYPE_CMMB = 0x00050100; + + /** The channel type for T-DMB (terrestrial). */ + public static final int TYPE_T_DMB = 0x00060000; + + /** The channel type for S-DMB (satellite). */ + public static final int TYPE_S_DMB = 0x00060100; + + /** + * The name of the TV input service that provides this TV channel. + * <p> + * This is a required field. + * </p><p> + * Type: TEXT + * </p> + */ + public static final String SERVICE_NAME = "service_name"; + + /** + * The predefined type of this TV channel. + * <p> + * This is used to indicate which broadcast standard (e.g. ATSC, DVB or ISDB) the current + * channel conforms to. + * </p><p> + * This is a required field. + * </p><p> + * Type: INTEGER + * </p> + */ + public static final String TYPE = "type"; + + /** + * The transport stream ID as appeared in various broadcast standards. + * <p> + * This is not a required field but if provided, can significantly increase the accuracy of + * channel identification. + * </p><p> + * Type: INTEGER + * </p> + */ + public static final String TRANSPORT_STREAM_ID = "transport_stream_id"; + + /** + * The channel number that is displayed to the user. + * <p> + * The format can vary depending on broadcast standard and product specification. + * </p><p> + * Type: INTEGER + * </p> + */ + public static final String DISPLAY_NUMBER = "display_number"; + + /** + * The channel name that is displayed to the user. + * <p> + * A call sign is a good candidate to use for this purpose but any name that helps the user + * recognize the current channel will be enough. Can also be empty depending on broadcast + * standard. + * </p><p> + * Type: TEXT + * </p> + */ + public static final String DISPLAY_NAME = "display_name"; + + /** + * The description of this TV channel. + * <p> + * Can be empty initially. + * </p><p> + * Type: TEXT + * </p> + */ + public static final String DESCRIPTION = "description"; + + /** + * The flag indicating whether this TV channel is browsable or not. + * <p> + * A value of 1 indicates the channel is included in the channel list that applications use + * to browse channels, a value of 0 indicates the channel is not included in the list. If + * not specified, this value is set to 1 by default. + * </p><p> + * Type: INTEGER (boolean) + * </p> + */ + public static final String BROWSABLE = "browsable"; + + /** + * Generic data used by individual TV input services. + * <p> + * Type: BLOB + * </p> + */ + public static final String DATA = "data"; + + + /** + * The version number of this row entry used by TV input services. + * <p> + * This is best used by sync adapters to identify the rows to update. The number can be + * defined by individual TV input services. One may assign the same value as + * {@code version_number} that appears in ETSI EN 300 468 or ATSC A/65, if the data are + * coming from a TV broadcast. + * </p><p> + * Type: INTEGER + * </p> + */ + public static final String VERSION_NUMBER = "version_number"; + + private Channels() {} + } + + /** Column definitions for the TV programs table. */ + public static final class Programs implements BaseTvColumns { + + /** The content:// style URI for this table. */ + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/program"); + + /** The MIME type of a directory of TV programs. */ + public static final String CONTENT_TYPE = + "vnd.android.cursor.dir/vnd.com.android.tv.programs"; + + /** The MIME type of a single TV program. */ + public static final String CONTENT_ITEM_TYPE = + "vnd.android.cursor.item/vnd.com.android.tv.programs"; + + /** + * The ID of the TV channel that contains this TV program. + * <p> + * This is a part of the channel URI and matches to {@link BaseColumns#_ID}. + * </p><p> + * Type: INTEGER (long) + * </p> + */ + public static final String CHANNEL_ID = "channel_id"; + + /** + * The title of this TV program. + * <p> + * Type: TEXT + * </p> + **/ + public static final String TITLE = "title"; + + /** + * The start time of this TV program, in milliseconds since the epoch. + * <p> + * Type: INTEGER (long) + * </p> + */ + public static final String START_TIME_UTC_MILLIS = "start_time_utc_millis"; + + /** + * The end time of this TV program, in milliseconds since the epoch. + * <p> + * Type: INTEGER (long) + * </p> + */ + public static final String END_TIME_UTC_MILLIS = "end_time_utc_millis"; + + /** + * The description of this TV program that is displayed to the user by default. + * <p> + * The maximum length of this field is 256 characters. + * </p><p> + * Type: TEXT + * </p> + */ + public static final String DESCRIPTION = "description"; + + /** + * The detailed, lengthy description of this TV program that is displayed only when the user + * wants to see more information. + * <p> + * TV input services should leave this field empty if they have no additional + * details beyond {@link #DESCRIPTION}. + * </p><p> + * Type: TEXT + * </p> + */ + public static final String LONG_DESCRIPTION = "long_description"; + + /** + * Generic data used by TV input services. + * <p> + * Type: BLOB + * </p> + */ + public static final String DATA = "data"; + + /** + * The version number of this row entry used by TV input services. + * <p> + * This is best used by sync adapters to identify the rows to update. The number can be + * defined by individual TV input services. One may assign the same value as + * {@code version_number} in ETSI EN 300 468 or ATSC A/65, if the data are coming from a TV + * broadcast. + * </p><p> + * Type: INTEGER + * </p> + */ + public static final String VERSION_NUMBER = "version_number"; + + private Programs() {} + } + + /** + * Column definitions for the TV programs that the user watched. Applications do not have access + * to this table. + * + * @hide + */ + public static final class WatchedPrograms implements BaseColumns { + + /** The content:// style URI for this table. */ + public static final Uri CONTENT_URI = + Uri.parse("content://" + AUTHORITY + "/watched_program"); + + /** The MIME type of a directory of watched programs. */ + public static final String CONTENT_TYPE = + "vnd.android.cursor.dir/vnd.com.android.tv.watched_programs"; + + /** The MIME type of a single item in this table. */ + public static final String CONTENT_ITEM_TYPE = + "vnd.android.cursor.item/vnd.com.android.tv.watched_programs"; + + /** + * The UTC time that the user started watching this TV program, in milliseconds since the + * epoch. + * <p> + * Type: INTEGER (long) + * </p> + */ + public static final String WATCH_START_TIME_UTC_MILLIS = "watch_start_time_utc_millis"; + + /** + * The UTC time that the user stopped watching this TV program, in milliseconds since the + * epoch. + * <p> + * Type: INTEGER (long) + * </p> + */ + public static final String WATCH_END_TIME_UTC_MILLIS = "watch_end_time_utc_millis"; + + /** + * The channel ID that contains this TV program. + * <p> + * Type: INTEGER (long) + * </p> + */ + public static final String CHANNEL_ID = "channel_id"; + + /** + * The title of this TV program. + * <p> + * Type: TEXT + * </p> + */ + public static final String TITLE = "title"; + + /** + * The start time of this TV program, in milliseconds since the epoch. + * <p> + * Type: INTEGER (long) + * </p> + */ + public static final String START_TIME_UTC_MILLIS = "start_time_utc_millis"; + + /** + * The end time of this TV program, in milliseconds since the epoch. + * <p> + * Type: INTEGER (long) + * </p> + */ + public static final String END_TIME_UTC_MILLIS = "end_time_utc_millis"; + + /** + * The description of this TV program. + * <p> + * Type: TEXT + * </p> + */ + public static final String DESCRIPTION = "description"; + + private WatchedPrograms() {} + } +} diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java index 8eaee29..3673f03 100644 --- a/core/java/android/service/notification/NotificationListenerService.java +++ b/core/java/android/service/notification/NotificationListenerService.java @@ -23,6 +23,7 @@ import android.content.Context; import android.content.Intent; import android.os.IBinder; import android.os.ServiceManager; +import android.os.UserHandle; import android.util.Log; /** @@ -121,11 +122,43 @@ public abstract class NotificationListenerService extends Service { * {@link android.app.NotificationManager#notify(String, int, android.app.Notification)}. * @param id ID of the notification as specified by the notifying app in * {@link android.app.NotificationManager#notify(String, int, android.app.Notification)}. + * <p> + * @deprecated Use {@link #cancelNotification(String key)} + * instead. Beginning with {@link android.os.Build.VERSION_CODES#L} this method will no longer + * cancel the notification. It will continue to cancel the notification for applications + * whose {@code targetSdkVersion} is earlier than {@link android.os.Build.VERSION_CODES#L}. */ public final void cancelNotification(String pkg, String tag, int id) { if (!isBound()) return; try { - getNotificationInterface().cancelNotificationFromListener(mWrapper, pkg, tag, id); + getNotificationInterface().cancelNotificationFromListener( + mWrapper, pkg, tag, id); + } catch (android.os.RemoteException ex) { + Log.v(TAG, "Unable to contact notification manager", ex); + } + } + + /** + * Inform the notification manager about dismissal of a single notification. + * <p> + * Use this if your listener has a user interface that allows the user to dismiss individual + * notifications, similar to the behavior of Android's status bar and notification panel. + * It should be called after the user dismisses a single notification using your UI; + * upon being informed, the notification manager will actually remove the notification + * and you will get an {@link #onNotificationRemoved(StatusBarNotification)} callback. + * <P> + * <b>Note:</b> If your listener allows the user to fire a notification's + * {@link android.app.Notification#contentIntent} by tapping/clicking/etc., you should call + * this method at that time <i>if</i> the Notification in question has the + * {@link android.app.Notification#FLAG_AUTO_CANCEL} flag set. + * <p> + * @param key Notification to dismiss from {@link StatusBarNotification#getKey()}. + */ + public final void cancelNotification(String key) { + if (!isBound()) return; + try { + getNotificationInterface().cancelNotificationsFromListener(mWrapper, + new String[] {key}); } catch (android.os.RemoteException ex) { Log.v(TAG, "Unable to contact notification manager", ex); } @@ -192,6 +225,24 @@ public abstract class NotificationListenerService extends Service { return null; } + /** + * Request the list of outstanding notification keys(that is, those that are visible to the + * current user). You can use the notification keys for subsequent retrieval via + * {@link #getActiveNotifications(String[]) or dismissal via + * {@link #cancelNotifications(String[]). + * + * @return An array of active notification keys. + */ + public String[] getActiveNotificationKeys() { + if (!isBound()) return null; + try { + return getNotificationInterface().getActiveNotificationKeysFromListener(mWrapper); + } catch (android.os.RemoteException ex) { + Log.v(TAG, "Unable to contact notification manager", ex); + } + return null; + } + @Override public IBinder onBind(Intent intent) { if (mWrapper == null) { diff --git a/core/java/android/service/notification/StatusBarNotification.java b/core/java/android/service/notification/StatusBarNotification.java index 96dd143d..2c0b76d 100644 --- a/core/java/android/service/notification/StatusBarNotification.java +++ b/core/java/android/service/notification/StatusBarNotification.java @@ -32,7 +32,7 @@ public class StatusBarNotification implements Parcelable { private final String key; private final int uid; - private final String basePkg; + private final String opPkg; private final int initialPid; private final Notification notification; private final UserHandle user; @@ -41,26 +41,20 @@ public class StatusBarNotification implements Parcelable { private final int score; /** @hide */ - public StatusBarNotification(String pkg, int id, String tag, int uid, int initialPid, int score, - Notification notification, UserHandle user) { - this(pkg, null, id, tag, uid, initialPid, score, notification, user); - } - - /** @hide */ - public StatusBarNotification(String pkg, String basePkg, int id, String tag, int uid, + public StatusBarNotification(String pkg, String opPkg, int id, String tag, int uid, int initialPid, int score, Notification notification, UserHandle user) { - this(pkg, basePkg, id, tag, uid, initialPid, score, notification, user, + this(pkg, opPkg, id, tag, uid, initialPid, score, notification, user, System.currentTimeMillis()); } - public StatusBarNotification(String pkg, String basePkg, int id, String tag, int uid, + public StatusBarNotification(String pkg, String opPkg, int id, String tag, int uid, int initialPid, int score, Notification notification, UserHandle user, long postTime) { if (pkg == null) throw new NullPointerException(); if (notification == null) throw new NullPointerException(); this.pkg = pkg; - this.basePkg = pkg; + this.opPkg = opPkg; this.id = id; this.tag = tag; this.uid = uid; @@ -69,14 +63,13 @@ public class StatusBarNotification implements Parcelable { this.notification = notification; this.user = user; this.notification.setUser(user); - this.postTime = postTime; this.key = key(); } public StatusBarNotification(Parcel in) { this.pkg = in.readString(); - this.basePkg = in.readString(); + this.opPkg = in.readString(); this.id = in.readInt(); if (in.readInt() != 0) { this.tag = in.readString(); @@ -94,12 +87,12 @@ public class StatusBarNotification implements Parcelable { } private String key() { - return pkg + '|' + basePkg + '|' + id + '|' + tag + '|' + uid; + return pkg + '|' + opPkg + '|' + id + '|' + tag + '|' + uid; } public void writeToParcel(Parcel out, int flags) { out.writeString(this.pkg); - out.writeString(this.basePkg); + out.writeString(this.opPkg); out.writeInt(this.id); if (this.tag != null) { out.writeInt(1); @@ -140,14 +133,14 @@ public class StatusBarNotification implements Parcelable { public StatusBarNotification cloneLight() { final Notification no = new Notification(); this.notification.cloneInto(no, false); // light copy - return new StatusBarNotification(this.pkg, this.basePkg, + return new StatusBarNotification(this.pkg, this.opPkg, this.id, this.tag, this.uid, this.initialPid, this.score, no, this.user, this.postTime); } @Override public StatusBarNotification clone() { - return new StatusBarNotification(this.pkg, this.basePkg, + return new StatusBarNotification(this.pkg, this.opPkg, this.id, this.tag, this.uid, this.initialPid, this.score, this.notification.clone(), this.user, this.postTime); } @@ -176,7 +169,11 @@ public class StatusBarNotification implements Parcelable { && ((notification.flags & Notification.FLAG_NO_CLEAR) == 0); } - /** Returns a userHandle for the instance of the app that posted this notification. */ + /** + * Returns a userHandle for the instance of the app that posted this notification. + * + * @deprecated Use {@link #getUser()} instead. + */ public int getUserId() { return this.user.getIdentifier(); } @@ -202,9 +199,9 @@ public class StatusBarNotification implements Parcelable { return uid; } - /** The notifying app's base package. @hide */ - public String getBasePkg() { - return basePkg; + /** The package used for AppOps tracking. @hide */ + public String getOpPkg() { + return opPkg; } /** @hide */ @@ -220,7 +217,6 @@ public class StatusBarNotification implements Parcelable { /** * The {@link android.os.UserHandle} for whom this notification is intended. - * @hide */ public UserHandle getUser() { return user; diff --git a/core/java/android/service/textservice/SpellCheckerService.java b/core/java/android/service/textservice/SpellCheckerService.java index 77b22ed..acfef82 100644 --- a/core/java/android/service/textservice/SpellCheckerService.java +++ b/core/java/android/service/textservice/SpellCheckerService.java @@ -32,7 +32,6 @@ import android.util.Log; import android.view.textservice.SentenceSuggestionsInfo; import android.view.textservice.SuggestionsInfo; import android.view.textservice.TextInfo; -import android.widget.SpellChecker; import java.lang.ref.WeakReference; import java.text.BreakIterator; diff --git a/core/java/android/service/trust/ITrustAgentService.aidl b/core/java/android/service/trust/ITrustAgentService.aidl new file mode 100644 index 0000000..863a249 --- /dev/null +++ b/core/java/android/service/trust/ITrustAgentService.aidl @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2014 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.service.trust; + +import android.os.Bundle; +import android.service.trust.ITrustAgentServiceCallback; + +/** + * Communication channel from TrustManagerService to the TrustAgent. + * @hide + */ +oneway interface ITrustAgentService { + void onUnlockAttempt(boolean successful); + void setCallback(ITrustAgentServiceCallback callback); +} diff --git a/core/java/android/service/trust/ITrustAgentServiceCallback.aidl b/core/java/android/service/trust/ITrustAgentServiceCallback.aidl new file mode 100644 index 0000000..c346771 --- /dev/null +++ b/core/java/android/service/trust/ITrustAgentServiceCallback.aidl @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2014 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.service.trust; + +import android.os.Bundle; +import android.os.UserHandle; + +/** + * Communication channel from the TrustAgentService back to TrustManagerService. + * @hide + */ +oneway interface ITrustAgentServiceCallback { + void enableTrust(String message, long durationMs, boolean initiatedByUser); + void revokeTrust(); +} diff --git a/core/java/android/service/trust/TrustAgentService.java b/core/java/android/service/trust/TrustAgentService.java new file mode 100644 index 0000000..d5ce429 --- /dev/null +++ b/core/java/android/service/trust/TrustAgentService.java @@ -0,0 +1,148 @@ +/** + * Copyright (C) 2014 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.service.trust; + +import android.annotation.SdkConstant; +import android.app.Service; +import android.content.Intent; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Slog; + +/** + * A service that notifies the system about whether it believes the environment of the device + * to be trusted. + * + * <p>To extend this class, you must declare the service in your manifest file with + * the {@link android.Manifest.permission#BIND_TRUST_AGENT_SERVICE} permission + * and include an intent filter with the {@link #SERVICE_INTERFACE} action. For example:</p> + * <pre> + * <service android:name=".TrustAgent" + * android:label="@string/service_name" + * android:permission="android.permission.BIND_TRUST_AGENT_SERVICE"> + * <intent-filter> + * <action android:name="android.service.trust.TrustAgentService" /> + * </intent-filter> + * <meta-data android:name="android.service.trust.trustagent" + * android:value="@xml/trust_agent" /> + * </service></pre> + * + * <p>The associated meta-data file can specify an activity that is accessible through Settings + * and should allow configuring the trust agent, as defined in + * {@link android.R.styleable#TrustAgent}. For example:</p> + * + * <pre> + * <trust_agent xmlns:android="http://schemas.android.com/apk/res/android" + * android:settingsActivity=".TrustAgentSettings" /></pre> + */ +public class TrustAgentService extends Service { + private final String TAG = TrustAgentService.class.getSimpleName() + + "[" + getClass().getSimpleName() + "]"; + + /** + * The {@link Intent} that must be declared as handled by the service. + */ + @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) + public static final String SERVICE_INTERFACE + = "android.service.trust.TrustAgentService"; + + /** + * The name of the {@code meta-data} tag pointing to additional configuration of the trust + * agent. + */ + public static final String TRUST_AGENT_META_DATA = "android.service.trust.trustagent"; + + private static final int MSG_UNLOCK_ATTEMPT = 1; + + private static final boolean DEBUG = false; + + private ITrustAgentServiceCallback mCallback; + + private Handler mHandler = new Handler() { + public void handleMessage(android.os.Message msg) { + switch (msg.what) { + case MSG_UNLOCK_ATTEMPT: + onUnlockAttempt(msg.arg1 != 0); + break; + } + }; + }; + + /** + * Called when the user attempted to authenticate on the device. + * + * @param successful true if the attempt succeeded + */ + protected void onUnlockAttempt(boolean successful) { + } + + private void onError(String msg) { + Slog.v(TAG, "Remote exception while " + msg); + } + + /** + * Call to enable trust on the device. + * + * @param message describes why the device is trusted, e.g. "Trusted by location". + * @param durationMs amount of time in milliseconds to keep the device in a trusted state. Trust + * for this agent will automatically be revoked when the timeout expires. + * @param initiatedByUser indicates that the user has explicitly initiated an action that proves + * the user is about to use the device. + */ + protected final void enableTrust(String message, long durationMs, boolean initiatedByUser) { + if (mCallback != null) { + try { + mCallback.enableTrust(message, durationMs, initiatedByUser); + } catch (RemoteException e) { + onError("calling enableTrust()"); + } + } + } + + /** + * Call to revoke trust on the device. + */ + protected final void revokeTrust() { + if (mCallback != null) { + try { + mCallback.revokeTrust(); + } catch (RemoteException e) { + onError("calling revokeTrust()"); + } + } + } + + @Override + public final IBinder onBind(Intent intent) { + if (DEBUG) Slog.v(TAG, "onBind() intent = " + intent); + return new TrustAgentServiceWrapper(); + } + + private final class TrustAgentServiceWrapper extends ITrustAgentService.Stub { + @Override + public void onUnlockAttempt(boolean successful) { + mHandler.obtainMessage(MSG_UNLOCK_ATTEMPT, successful ? 1 : 0, 0) + .sendToTarget(); + } + + public void setCallback(ITrustAgentServiceCallback callback) { + mCallback = callback; + } + } + +} diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index 5db8168..03ce4e0 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -38,7 +38,6 @@ import android.os.Message; import android.os.PowerManager; import android.os.RemoteException; import android.util.Log; -import android.util.LogPrinter; import android.view.Display; import android.view.Gravity; import android.view.IWindowSession; diff --git a/core/java/android/speech/srec/Recognizer.java b/core/java/android/speech/srec/Recognizer.java index 1396204..6c491a0 100644 --- a/core/java/android/speech/srec/Recognizer.java +++ b/core/java/android/speech/srec/Recognizer.java @@ -22,8 +22,6 @@ package android.speech.srec; -import android.util.Log; - import java.io.File; import java.io.InputStream; import java.io.IOException; diff --git a/core/java/android/speech/tts/AbstractEventLogger.java b/core/java/android/speech/tts/AbstractEventLogger.java new file mode 100644 index 0000000..37f8656 --- /dev/null +++ b/core/java/android/speech/tts/AbstractEventLogger.java @@ -0,0 +1,124 @@ +/* + * 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.speech.tts; + +import android.os.SystemClock; + +/** + * Base class for storing data about a given speech synthesis request to the + * event logs. The data that is logged depends on actual implementation. Note + * that {@link AbstractEventLogger#onAudioDataWritten()} and + * {@link AbstractEventLogger#onEngineComplete()} must be called from a single + * thread (usually the audio playback thread}. + */ +abstract class AbstractEventLogger { + protected final String mServiceApp; + protected final int mCallerUid; + protected final int mCallerPid; + protected final long mReceivedTime; + protected long mPlaybackStartTime = -1; + + private volatile long mRequestProcessingStartTime = -1; + private volatile long mEngineStartTime = -1; + private volatile long mEngineCompleteTime = -1; + + private boolean mLogWritten = false; + + AbstractEventLogger(int callerUid, int callerPid, String serviceApp) { + mCallerUid = callerUid; + mCallerPid = callerPid; + mServiceApp = serviceApp; + mReceivedTime = SystemClock.elapsedRealtime(); + } + + /** + * Notifies the logger that this request has been selected from + * the processing queue for processing. Engine latency / total time + * is measured from this baseline. + */ + public void onRequestProcessingStart() { + mRequestProcessingStartTime = SystemClock.elapsedRealtime(); + } + + /** + * Notifies the logger that a chunk of data has been received from + * the engine. Might be called multiple times. + */ + public void onEngineDataReceived() { + if (mEngineStartTime == -1) { + mEngineStartTime = SystemClock.elapsedRealtime(); + } + } + + /** + * Notifies the logger that the engine has finished processing data. + * Will be called exactly once. + */ + public void onEngineComplete() { + mEngineCompleteTime = SystemClock.elapsedRealtime(); + } + + /** + * Notifies the logger that audio playback has started for some section + * of the synthesis. This is normally some amount of time after the engine + * has synthesized data and varies depending on utterances and + * other audio currently in the queue. + */ + public void onAudioDataWritten() { + // For now, keep track of only the first chunk of audio + // that was played. + if (mPlaybackStartTime == -1) { + mPlaybackStartTime = SystemClock.elapsedRealtime(); + } + } + + /** + * Notifies the logger that the current synthesis has completed. + * All available data is not logged. + */ + public void onCompleted(int statusCode) { + if (mLogWritten) { + return; + } else { + mLogWritten = true; + } + + long completionTime = SystemClock.elapsedRealtime(); + + // We don't report latency for stopped syntheses because their overall + // total time spent will be inaccurate (will not correlate with + // the length of the utterance). + + // onAudioDataWritten() should normally always be called, and hence mPlaybackStartTime + // should be set, if an error does not occur. + if (statusCode != TextToSpeechClient.Status.SUCCESS + || mPlaybackStartTime == -1 || mEngineCompleteTime == -1) { + logFailure(statusCode); + return; + } + + final long audioLatency = mPlaybackStartTime - mReceivedTime; + final long engineLatency = mEngineStartTime - mRequestProcessingStartTime; + final long engineTotal = mEngineCompleteTime - mRequestProcessingStartTime; + logSuccess(audioLatency, engineLatency, engineTotal); + } + + protected abstract void logFailure(int statusCode); + protected abstract void logSuccess(long audioLatency, long engineLatency, + long engineTotal); + + +} diff --git a/core/java/android/speech/tts/AbstractSynthesisCallback.java b/core/java/android/speech/tts/AbstractSynthesisCallback.java index c7a4af0..91e119b 100644 --- a/core/java/android/speech/tts/AbstractSynthesisCallback.java +++ b/core/java/android/speech/tts/AbstractSynthesisCallback.java @@ -15,15 +15,28 @@ */ package android.speech.tts; + /** * Defines additional methods the synthesis callback must implement that * are private to the TTS service implementation. + * + * All of these class methods (with the exception of {@link #stop()}) can be only called on the + * synthesis thread, while inside + * {@link TextToSpeechService#onSynthesizeText} or {@link TextToSpeechService#onSynthesizeTextV2}. + * {@link #stop()} is the exception, it may be called from multiple threads. */ abstract class AbstractSynthesisCallback implements SynthesisCallback { + /** If true, request comes from V2 TTS interface */ + protected final boolean mClientIsUsingV2; + /** - * Checks whether the synthesis request completed successfully. + * Constructor. + * @param clientIsUsingV2 If true, this callback will be used inside + * {@link TextToSpeechService#onSynthesizeTextV2} method. */ - abstract boolean isDone(); + AbstractSynthesisCallback(boolean clientIsUsingV2) { + mClientIsUsingV2 = clientIsUsingV2; + } /** * Aborts the speech request. @@ -31,4 +44,16 @@ abstract class AbstractSynthesisCallback implements SynthesisCallback { * Can be called from multiple threads. */ abstract void stop(); + + /** + * Get status code for a "stop". + * + * V2 Clients will receive special status, V1 clients will receive standard error. + * + * This method should only be called on the synthesis thread, + * while in {@link TextToSpeechService#onSynthesizeText}. + */ + int errorCodeOnStop() { + return mClientIsUsingV2 ? TextToSpeechClient.Status.STOPPED : TextToSpeech.ERROR; + } } diff --git a/core/java/android/speech/tts/AudioPlaybackHandler.java b/core/java/android/speech/tts/AudioPlaybackHandler.java index d63f605..dcf49b0 100644 --- a/core/java/android/speech/tts/AudioPlaybackHandler.java +++ b/core/java/android/speech/tts/AudioPlaybackHandler.java @@ -43,7 +43,7 @@ class AudioPlaybackHandler { return; } - item.stop(false); + item.stop(TextToSpeechClient.Status.STOPPED); } public void enqueue(PlaybackQueueItem item) { diff --git a/core/java/android/speech/tts/AudioPlaybackQueueItem.java b/core/java/android/speech/tts/AudioPlaybackQueueItem.java index 1a1fda8..c514639 100644 --- a/core/java/android/speech/tts/AudioPlaybackQueueItem.java +++ b/core/java/android/speech/tts/AudioPlaybackQueueItem.java @@ -53,7 +53,7 @@ class AudioPlaybackQueueItem extends PlaybackQueueItem { dispatcher.dispatchOnStart(); mPlayer = MediaPlayer.create(mContext, mUri); if (mPlayer == null) { - dispatcher.dispatchOnError(); + dispatcher.dispatchOnError(TextToSpeechClient.Status.ERROR_OUTPUT); return; } @@ -83,9 +83,9 @@ class AudioPlaybackQueueItem extends PlaybackQueueItem { } if (mFinished) { - dispatcher.dispatchOnDone(); + dispatcher.dispatchOnSuccess(); } else { - dispatcher.dispatchOnError(); + dispatcher.dispatchOnStop(); } } @@ -99,7 +99,7 @@ class AudioPlaybackQueueItem extends PlaybackQueueItem { } @Override - void stop(boolean isError) { + void stop(int errorCode) { mDone.open(); } } diff --git a/core/java/android/speech/tts/EventLogTags.logtags b/core/java/android/speech/tts/EventLogTags.logtags index f8654ad..e209a28 100644 --- a/core/java/android/speech/tts/EventLogTags.logtags +++ b/core/java/android/speech/tts/EventLogTags.logtags @@ -4,3 +4,6 @@ option java_package android.speech.tts; 76001 tts_speak_success (engine|3),(caller_uid|1),(caller_pid|1),(length|1),(locale|3),(rate|1),(pitch|1),(engine_latency|2|3),(engine_total|2|3),(audio_latency|2|3) 76002 tts_speak_failure (engine|3),(caller_uid|1),(caller_pid|1),(length|1),(locale|3),(rate|1),(pitch|1) + +76003 tts_v2_speak_success (engine|3),(caller_uid|1),(caller_pid|1),(length|1),(request_config|3),(engine_latency|2|3),(engine_total|2|3),(audio_latency|2|3) +76004 tts_v2_speak_failure (engine|3),(caller_uid|1),(caller_pid|1),(length|1),(request_config|3), (statusCode|1) diff --git a/core/java/android/speech/tts/EventLogger.java b/core/java/android/speech/tts/EventLogger.java deleted file mode 100644 index 82ed4dd..0000000 --- a/core/java/android/speech/tts/EventLogger.java +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright (C) 2011 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.speech.tts; - -import android.os.SystemClock; -import android.text.TextUtils; -import android.util.Log; - -/** - * Writes data about a given speech synthesis request to the event logs. - * The data that is logged includes the calling app, length of the utterance, - * speech rate / pitch and the latency and overall time taken. - * - * Note that {@link EventLogger#onStopped()} and {@link EventLogger#onError()} - * might be called from any thread, but on {@link EventLogger#onAudioDataWritten()} and - * {@link EventLogger#onComplete()} must be called from a single thread - * (usually the audio playback thread} - */ -class EventLogger { - private final SynthesisRequest mRequest; - private final String mServiceApp; - private final int mCallerUid; - private final int mCallerPid; - private final long mReceivedTime; - private long mPlaybackStartTime = -1; - private volatile long mRequestProcessingStartTime = -1; - private volatile long mEngineStartTime = -1; - private volatile long mEngineCompleteTime = -1; - - private volatile boolean mError = false; - private volatile boolean mStopped = false; - private boolean mLogWritten = false; - - EventLogger(SynthesisRequest request, int callerUid, int callerPid, String serviceApp) { - mRequest = request; - mCallerUid = callerUid; - mCallerPid = callerPid; - mServiceApp = serviceApp; - mReceivedTime = SystemClock.elapsedRealtime(); - } - - /** - * Notifies the logger that this request has been selected from - * the processing queue for processing. Engine latency / total time - * is measured from this baseline. - */ - public void onRequestProcessingStart() { - mRequestProcessingStartTime = SystemClock.elapsedRealtime(); - } - - /** - * Notifies the logger that a chunk of data has been received from - * the engine. Might be called multiple times. - */ - public void onEngineDataReceived() { - if (mEngineStartTime == -1) { - mEngineStartTime = SystemClock.elapsedRealtime(); - } - } - - /** - * Notifies the logger that the engine has finished processing data. - * Will be called exactly once. - */ - public void onEngineComplete() { - mEngineCompleteTime = SystemClock.elapsedRealtime(); - } - - /** - * Notifies the logger that audio playback has started for some section - * of the synthesis. This is normally some amount of time after the engine - * has synthesized data and varies depending on utterances and - * other audio currently in the queue. - */ - public void onAudioDataWritten() { - // For now, keep track of only the first chunk of audio - // that was played. - if (mPlaybackStartTime == -1) { - mPlaybackStartTime = SystemClock.elapsedRealtime(); - } - } - - /** - * Notifies the logger that the current synthesis was stopped. - * Latency numbers are not reported for stopped syntheses. - */ - public void onStopped() { - mStopped = false; - } - - /** - * Notifies the logger that the current synthesis resulted in - * an error. This is logged using {@link EventLogTags#writeTtsSpeakFailure}. - */ - public void onError() { - mError = true; - } - - /** - * Notifies the logger that the current synthesis has completed. - * All available data is not logged. - */ - public void onWriteData() { - if (mLogWritten) { - return; - } else { - mLogWritten = true; - } - - long completionTime = SystemClock.elapsedRealtime(); - // onAudioDataWritten() should normally always be called if an - // error does not occur. - if (mError || mPlaybackStartTime == -1 || mEngineCompleteTime == -1) { - EventLogTags.writeTtsSpeakFailure(mServiceApp, mCallerUid, mCallerPid, - getUtteranceLength(), getLocaleString(), - mRequest.getSpeechRate(), mRequest.getPitch()); - return; - } - - // We don't report stopped syntheses because their overall - // total time spent will be innacurate (will not correlate with - // the length of the utterance). - if (mStopped) { - return; - } - - final long audioLatency = mPlaybackStartTime - mReceivedTime; - final long engineLatency = mEngineStartTime - mRequestProcessingStartTime; - final long engineTotal = mEngineCompleteTime - mRequestProcessingStartTime; - - EventLogTags.writeTtsSpeakSuccess(mServiceApp, mCallerUid, mCallerPid, - getUtteranceLength(), getLocaleString(), - mRequest.getSpeechRate(), mRequest.getPitch(), - engineLatency, engineTotal, audioLatency); - } - - /** - * @return the length of the utterance for the given synthesis, 0 - * if the utterance was {@code null}. - */ - private int getUtteranceLength() { - final String utterance = mRequest.getText(); - return utterance == null ? 0 : utterance.length(); - } - - /** - * Returns a formatted locale string from the synthesis params of the - * form lang-country-variant. - */ - private String getLocaleString() { - StringBuilder sb = new StringBuilder(mRequest.getLanguage()); - if (!TextUtils.isEmpty(mRequest.getCountry())) { - sb.append('-'); - sb.append(mRequest.getCountry()); - - if (!TextUtils.isEmpty(mRequest.getVariant())) { - sb.append('-'); - sb.append(mRequest.getVariant()); - } - } - - return sb.toString(); - } - -} diff --git a/core/java/android/speech/tts/EventLoggerV1.java b/core/java/android/speech/tts/EventLoggerV1.java new file mode 100644 index 0000000..f484347 --- /dev/null +++ b/core/java/android/speech/tts/EventLoggerV1.java @@ -0,0 +1,80 @@ +/* + * 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.speech.tts; + +import android.text.TextUtils; + +/** + * Writes data about a given speech synthesis request for V1 API to the event + * logs. The data that is logged includes the calling app, length of the + * utterance, speech rate / pitch, the latency, and overall time taken. + */ +class EventLoggerV1 extends AbstractEventLogger { + private final SynthesisRequest mRequest; + + EventLoggerV1(SynthesisRequest request, int callerUid, int callerPid, String serviceApp) { + super(callerUid, callerPid, serviceApp); + mRequest = request; + } + + @Override + protected void logFailure(int statusCode) { + // We don't report stopped syntheses because their overall + // total time spent will be inaccurate (will not correlate with + // the length of the utterance). + if (statusCode != TextToSpeechClient.Status.STOPPED) { + EventLogTags.writeTtsSpeakFailure(mServiceApp, mCallerUid, mCallerPid, + getUtteranceLength(), getLocaleString(), + mRequest.getSpeechRate(), mRequest.getPitch()); + } + } + + @Override + protected void logSuccess(long audioLatency, long engineLatency, long engineTotal) { + EventLogTags.writeTtsSpeakSuccess(mServiceApp, mCallerUid, mCallerPid, + getUtteranceLength(), getLocaleString(), + mRequest.getSpeechRate(), mRequest.getPitch(), + engineLatency, engineTotal, audioLatency); + } + + /** + * @return the length of the utterance for the given synthesis, 0 + * if the utterance was {@code null}. + */ + private int getUtteranceLength() { + final String utterance = mRequest.getText(); + return utterance == null ? 0 : utterance.length(); + } + + /** + * Returns a formatted locale string from the synthesis params of the + * form lang-country-variant. + */ + private String getLocaleString() { + StringBuilder sb = new StringBuilder(mRequest.getLanguage()); + if (!TextUtils.isEmpty(mRequest.getCountry())) { + sb.append('-'); + sb.append(mRequest.getCountry()); + + if (!TextUtils.isEmpty(mRequest.getVariant())) { + sb.append('-'); + sb.append(mRequest.getVariant()); + } + } + + return sb.toString(); + } +} diff --git a/core/java/android/speech/tts/EventLoggerV2.java b/core/java/android/speech/tts/EventLoggerV2.java new file mode 100644 index 0000000..b8e4dae --- /dev/null +++ b/core/java/android/speech/tts/EventLoggerV2.java @@ -0,0 +1,73 @@ +/* + * 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.speech.tts; + + + +/** + * Writes data about a given speech synthesis request for V2 API to the event logs. + * The data that is logged includes the calling app, length of the utterance, + * synthesis request configuration and the latency and overall time taken. + */ +class EventLoggerV2 extends AbstractEventLogger { + private final SynthesisRequestV2 mRequest; + + EventLoggerV2(SynthesisRequestV2 request, int callerUid, int callerPid, String serviceApp) { + super(callerUid, callerPid, serviceApp); + mRequest = request; + } + + @Override + protected void logFailure(int statusCode) { + // We don't report stopped syntheses because their overall + // total time spent will be inaccurate (will not correlate with + // the length of the utterance). + if (statusCode != TextToSpeechClient.Status.STOPPED) { + EventLogTags.writeTtsV2SpeakFailure(mServiceApp, + mCallerUid, mCallerPid, getUtteranceLength(), getRequestConfigString(), statusCode); + } + } + + @Override + protected void logSuccess(long audioLatency, long engineLatency, long engineTotal) { + EventLogTags.writeTtsV2SpeakSuccess(mServiceApp, + mCallerUid, mCallerPid, getUtteranceLength(), getRequestConfigString(), + engineLatency, engineTotal, audioLatency); + } + + /** + * @return the length of the utterance for the given synthesis, 0 + * if the utterance was {@code null}. + */ + private int getUtteranceLength() { + final String utterance = mRequest.getText(); + return utterance == null ? 0 : utterance.length(); + } + + /** + * Returns a string representation of the synthesis request configuration. + */ + private String getRequestConfigString() { + // Ensure the bundles are unparceled. + mRequest.getVoiceParams().size(); + mRequest.getAudioParams().size(); + + return new StringBuilder(64).append("VoiceName: ").append(mRequest.getVoiceName()) + .append(" ,VoiceParams: ").append(mRequest.getVoiceParams()) + .append(" ,SystemParams: ").append(mRequest.getAudioParams()) + .append("]").toString(); + } +} diff --git a/core/java/android/speech/tts/FileSynthesisCallback.java b/core/java/android/speech/tts/FileSynthesisCallback.java index ab8f82f..717aeb6 100644 --- a/core/java/android/speech/tts/FileSynthesisCallback.java +++ b/core/java/android/speech/tts/FileSynthesisCallback.java @@ -16,13 +16,10 @@ package android.speech.tts; import android.media.AudioFormat; -import android.os.FileUtils; +import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher; import android.util.Log; -import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.channels.FileChannel; @@ -48,19 +45,39 @@ class FileSynthesisCallback extends AbstractSynthesisCallback { private FileChannel mFileChannel; + private final UtteranceProgressDispatcher mDispatcher; + private final Object mCallerIdentity; + private boolean mStarted = false; - private boolean mStopped = false; private boolean mDone = false; - FileSynthesisCallback(FileChannel fileChannel) { + /** Status code of synthesis */ + protected int mStatusCode; + + FileSynthesisCallback(FileChannel fileChannel, UtteranceProgressDispatcher dispatcher, + Object callerIdentity, boolean clientIsUsingV2) { + super(clientIsUsingV2); mFileChannel = fileChannel; + mDispatcher = dispatcher; + mCallerIdentity = callerIdentity; + mStatusCode = TextToSpeechClient.Status.SUCCESS; } @Override void stop() { synchronized (mStateLock) { - mStopped = true; + if (mDone) { + return; + } + if (mStatusCode == TextToSpeechClient.Status.STOPPED) { + return; + } + + mStatusCode = TextToSpeechClient.Status.STOPPED; cleanUp(); + if (mDispatcher != null) { + mDispatcher.dispatchOnStop(); + } } } @@ -75,14 +92,8 @@ class FileSynthesisCallback extends AbstractSynthesisCallback { * Must be called while holding the monitor on {@link #mStateLock}. */ private void closeFile() { - try { - if (mFileChannel != null) { - mFileChannel.close(); - mFileChannel = null; - } - } catch (IOException ex) { - Log.e(TAG, "Failed to close output file descriptor", ex); - } + // File will be closed by the SpeechItem in the speech service. + mFileChannel = null; } @Override @@ -91,38 +102,46 @@ class FileSynthesisCallback extends AbstractSynthesisCallback { } @Override - boolean isDone() { - return mDone; - } - - @Override public int start(int sampleRateInHz, int audioFormat, int channelCount) { if (DBG) { Log.d(TAG, "FileSynthesisRequest.start(" + sampleRateInHz + "," + audioFormat + "," + channelCount + ")"); } + FileChannel fileChannel = null; synchronized (mStateLock) { - if (mStopped) { + if (mStatusCode == TextToSpeechClient.Status.STOPPED) { if (DBG) Log.d(TAG, "Request has been aborted."); + return errorCodeOnStop(); + } + if (mStatusCode != TextToSpeechClient.Status.SUCCESS) { + if (DBG) Log.d(TAG, "Error was raised"); return TextToSpeech.ERROR; } if (mStarted) { - cleanUp(); - throw new IllegalArgumentException("FileSynthesisRequest.start() called twice"); + Log.e(TAG, "Start called twice"); + return TextToSpeech.ERROR; } mStarted = true; mSampleRateInHz = sampleRateInHz; mAudioFormat = audioFormat; mChannelCount = channelCount; - try { - mFileChannel.write(ByteBuffer.allocate(WAV_HEADER_LENGTH)); + if (mDispatcher != null) { + mDispatcher.dispatchOnStart(); + } + fileChannel = mFileChannel; + } + + try { + fileChannel.write(ByteBuffer.allocate(WAV_HEADER_LENGTH)); return TextToSpeech.SUCCESS; - } catch (IOException ex) { - Log.e(TAG, "Failed to write wav header to output file descriptor" + ex); + } catch (IOException ex) { + Log.e(TAG, "Failed to write wav header to output file descriptor", ex); + synchronized (mStateLock) { cleanUp(); - return TextToSpeech.ERROR; + mStatusCode = TextToSpeechClient.Status.ERROR_OUTPUT; } + return TextToSpeech.ERROR; } } @@ -132,66 +151,128 @@ class FileSynthesisCallback extends AbstractSynthesisCallback { Log.d(TAG, "FileSynthesisRequest.audioAvailable(" + buffer + "," + offset + "," + length + ")"); } + FileChannel fileChannel = null; synchronized (mStateLock) { - if (mStopped) { + if (mStatusCode == TextToSpeechClient.Status.STOPPED) { if (DBG) Log.d(TAG, "Request has been aborted."); + return errorCodeOnStop(); + } + if (mStatusCode != TextToSpeechClient.Status.SUCCESS) { + if (DBG) Log.d(TAG, "Error was raised"); return TextToSpeech.ERROR; } if (mFileChannel == null) { Log.e(TAG, "File not open"); + mStatusCode = TextToSpeechClient.Status.ERROR_OUTPUT; return TextToSpeech.ERROR; } - try { - mFileChannel.write(ByteBuffer.wrap(buffer, offset, length)); - return TextToSpeech.SUCCESS; - } catch (IOException ex) { - Log.e(TAG, "Failed to write to output file descriptor", ex); - cleanUp(); + if (!mStarted) { + Log.e(TAG, "Start method was not called"); return TextToSpeech.ERROR; } + fileChannel = mFileChannel; + } + + try { + fileChannel.write(ByteBuffer.wrap(buffer, offset, length)); + return TextToSpeech.SUCCESS; + } catch (IOException ex) { + Log.e(TAG, "Failed to write to output file descriptor", ex); + synchronized (mStateLock) { + cleanUp(); + mStatusCode = TextToSpeechClient.Status.ERROR_OUTPUT; + } + return TextToSpeech.ERROR; } } @Override public int done() { if (DBG) Log.d(TAG, "FileSynthesisRequest.done()"); + FileChannel fileChannel = null; + + int sampleRateInHz = 0; + int audioFormat = 0; + int channelCount = 0; + synchronized (mStateLock) { if (mDone) { - if (DBG) Log.d(TAG, "Duplicate call to done()"); - // This preserves existing behaviour. Earlier, if done was called twice - // we'd return ERROR because mFile == null and we'd add to logspam. + Log.w(TAG, "Duplicate call to done()"); + // This is not an error that would prevent synthesis. Hence no + // setStatusCode is set. return TextToSpeech.ERROR; } - if (mStopped) { + if (mStatusCode == TextToSpeechClient.Status.STOPPED) { if (DBG) Log.d(TAG, "Request has been aborted."); + return errorCodeOnStop(); + } + if (mDispatcher != null && mStatusCode != TextToSpeechClient.Status.SUCCESS && + mStatusCode != TextToSpeechClient.Status.STOPPED) { + mDispatcher.dispatchOnError(mStatusCode); return TextToSpeech.ERROR; } if (mFileChannel == null) { Log.e(TAG, "File not open"); return TextToSpeech.ERROR; } - try { - // Write WAV header at start of file - mFileChannel.position(0); - int dataLength = (int) (mFileChannel.size() - WAV_HEADER_LENGTH); - mFileChannel.write( - makeWavHeader(mSampleRateInHz, mAudioFormat, mChannelCount, dataLength)); + mDone = true; + fileChannel = mFileChannel; + sampleRateInHz = mSampleRateInHz; + audioFormat = mAudioFormat; + channelCount = mChannelCount; + } + + try { + // Write WAV header at start of file + fileChannel.position(0); + int dataLength = (int) (fileChannel.size() - WAV_HEADER_LENGTH); + fileChannel.write( + makeWavHeader(sampleRateInHz, audioFormat, channelCount, dataLength)); + + synchronized (mStateLock) { closeFile(); - mDone = true; + if (mDispatcher != null) { + mDispatcher.dispatchOnSuccess(); + } return TextToSpeech.SUCCESS; - } catch (IOException ex) { - Log.e(TAG, "Failed to write to output file descriptor", ex); + } + } catch (IOException ex) { + Log.e(TAG, "Failed to write to output file descriptor", ex); + synchronized (mStateLock) { cleanUp(); - return TextToSpeech.ERROR; } + return TextToSpeech.ERROR; } } @Override public void error() { + error(TextToSpeechClient.Status.ERROR_SYNTHESIS); + } + + @Override + public void error(int errorCode) { if (DBG) Log.d(TAG, "FileSynthesisRequest.error()"); synchronized (mStateLock) { + if (mDone) { + return; + } cleanUp(); + mStatusCode = errorCode; + } + } + + @Override + public boolean hasStarted() { + synchronized (mStateLock) { + return mStarted; + } + } + + @Override + public boolean hasFinished() { + synchronized (mStateLock) { + return mDone; } } @@ -225,4 +306,16 @@ class FileSynthesisCallback extends AbstractSynthesisCallback { return header; } + @Override + public int fallback() { + synchronized (mStateLock) { + if (hasStarted() || hasFinished()) { + return TextToSpeech.ERROR; + } + + mDispatcher.dispatchOnFallback(); + mStatusCode = TextToSpeechClient.Status.SUCCESS; + return TextToSpeechClient.Status.SUCCESS; + } + } } diff --git a/core/java/android/speech/tts/ITextToSpeechCallback.aidl b/core/java/android/speech/tts/ITextToSpeechCallback.aidl index f0287d4..3c808ff 100644 --- a/core/java/android/speech/tts/ITextToSpeechCallback.aidl +++ b/core/java/android/speech/tts/ITextToSpeechCallback.aidl @@ -15,13 +15,53 @@ */ package android.speech.tts; +import android.speech.tts.VoiceInfo; + /** * Interface for callbacks from TextToSpeechService * * {@hide} */ oneway interface ITextToSpeechCallback { + /** + * Tells the client that the synthesis has started. + * + * @param utteranceId Unique id identifying synthesis request. + */ void onStart(String utteranceId); - void onDone(String utteranceId); - void onError(String utteranceId); + + /** + * Tells the client that the synthesis has finished. + * + * @param utteranceId Unique id identifying synthesis request. + */ + void onSuccess(String utteranceId); + + /** + * Tells the client that the synthesis was stopped. + * + * @param utteranceId Unique id identifying synthesis request. + */ + void onStop(String utteranceId); + + /** + * Tells the client that the synthesis failed, and fallback synthesis will be attempted. + * + * @param utteranceId Unique id identifying synthesis request. + */ + void onFallback(String utteranceId); + + /** + * Tells the client that the synthesis has failed. + * + * @param utteranceId Unique id identifying synthesis request. + * @param errorCode One of the values from + * {@link android.speech.tts.v2.TextToSpeechClient.Status}. + */ + void onError(String utteranceId, int errorCode); + + /** + * Inform the client that set of available voices changed. + */ + void onVoicesInfoChange(in List<VoiceInfo> voices); } diff --git a/core/java/android/speech/tts/ITextToSpeechService.aidl b/core/java/android/speech/tts/ITextToSpeechService.aidl index b7bc70c..9cf49ff 100644 --- a/core/java/android/speech/tts/ITextToSpeechService.aidl +++ b/core/java/android/speech/tts/ITextToSpeechService.aidl @@ -20,6 +20,8 @@ import android.net.Uri; import android.os.Bundle; import android.os.ParcelFileDescriptor; import android.speech.tts.ITextToSpeechCallback; +import android.speech.tts.VoiceInfo; +import android.speech.tts.SynthesisRequestV2; /** * Interface for TextToSpeech to talk to TextToSpeechService. @@ -70,9 +72,10 @@ interface ITextToSpeechService { * TextToSpeech object. * @param duration Number of milliseconds of silence to play. * @param queueMode Determines what to do to requests already in the queue. - * @param param Request parameters. + * @param utteranceId Unique id used to identify this request in callbacks. */ - int playSilence(in IBinder callingInstance, in long duration, in int queueMode, in Bundle params); + int playSilence(in IBinder callingInstance, in long duration, in int queueMode, + in String utteranceId); /** * Checks whether the service is currently playing some audio. @@ -90,7 +93,6 @@ interface ITextToSpeechService { /** * Returns the language, country and variant currently being used by the TTS engine. - * * Can be called from multiple threads. * * @return A 3-element array, containing language (ISO 3-letter code), @@ -99,7 +101,7 @@ interface ITextToSpeechService { * be empty too. */ String[] getLanguage(); - + /** * Returns a default TTS language, country and variant as set by the user. * @@ -111,7 +113,7 @@ interface ITextToSpeechService { * be empty too. */ String[] getClientDefaultLanguage(); - + /** * Checks whether the engine supports a given language. * @@ -137,7 +139,7 @@ interface ITextToSpeechService { * @param country ISO-3 country code. May be empty or null. * @param variant Language variant. May be empty or null. * @return An array of strings containing the set of features supported for - * the supplied locale. The array of strings must not contain + * the supplied locale. The array of strings must not contain * duplicates. */ String[] getFeaturesForLanguage(in String lang, in String country, in String variant); @@ -169,4 +171,44 @@ interface ITextToSpeechService { */ void setCallback(in IBinder caller, ITextToSpeechCallback cb); + /** + * Tells the engine to synthesize some speech and play it back. + * + * @param callingInstance a binder representing the identity of the calling + * TextToSpeech object. + * @param text The text to synthesize. + * @param queueMode Determines what to do to requests already in the queue. + * @param request Request parameters. + */ + int speakV2(in IBinder callingInstance, in SynthesisRequestV2 request); + + /** + * Tells the engine to synthesize some speech and write it to a file. + * + * @param callingInstance a binder representing the identity of the calling + * TextToSpeech object. + * @param text The text to synthesize. + * @param fileDescriptor The file descriptor to write the synthesized audio to. Has to be + writable. + * @param request Request parameters. + */ + int synthesizeToFileDescriptorV2(in IBinder callingInstance, + in ParcelFileDescriptor fileDescriptor, in SynthesisRequestV2 request); + + /** + * Plays an existing audio resource. V2 version + * + * @param callingInstance a binder representing the identity of the calling + * TextToSpeech object. + * @param audioUri URI for the audio resource (a file or android.resource URI) + * @param utteranceId Unique identifier. + * @param audioParameters Parameters for audio playback (from {@link SynthesisRequestV2}). + */ + int playAudioV2(in IBinder callingInstance, in Uri audioUri, in String utteranceId, + in Bundle audioParameters); + + /** + * Request the list of available voices from the service. + */ + List<VoiceInfo> getVoicesInfo(); } diff --git a/core/java/android/speech/tts/PlaybackQueueItem.java b/core/java/android/speech/tts/PlaybackQueueItem.java index d0957ff..b2e323e 100644 --- a/core/java/android/speech/tts/PlaybackQueueItem.java +++ b/core/java/android/speech/tts/PlaybackQueueItem.java @@ -22,6 +22,16 @@ abstract class PlaybackQueueItem implements Runnable { return mDispatcher; } + @Override public abstract void run(); - abstract void stop(boolean isError); + + /** + * Stop the playback. + * + * @param errorCode Cause of the stop. Can be either one of the error codes from + * {@link android.speech.tts.TextToSpeechClient.Status} or + * {@link android.speech.tts.TextToSpeechClient.Status#STOPPED} + * if stopped on a client request. + */ + abstract void stop(int errorCode); } diff --git a/core/java/android/speech/tts/PlaybackSynthesisCallback.java b/core/java/android/speech/tts/PlaybackSynthesisCallback.java index c99f201..e345e89 100644 --- a/core/java/android/speech/tts/PlaybackSynthesisCallback.java +++ b/core/java/android/speech/tts/PlaybackSynthesisCallback.java @@ -55,20 +55,20 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { private final AudioPlaybackHandler mAudioTrackHandler; // A request "token", which will be non null after start() has been called. private SynthesisPlaybackQueueItem mItem = null; - // Whether this request has been stopped. This is useful for keeping - // track whether stop() has been called before start(). In all other cases, - // a non-null value of mItem will provide the same information. - private boolean mStopped = false; private volatile boolean mDone = false; + /** Status code of synthesis */ + protected int mStatusCode; + private final UtteranceProgressDispatcher mDispatcher; private final Object mCallerIdentity; - private final EventLogger mLogger; + private final AbstractEventLogger mLogger; PlaybackSynthesisCallback(int streamType, float volume, float pan, AudioPlaybackHandler audioTrackHandler, UtteranceProgressDispatcher dispatcher, - Object callerIdentity, EventLogger logger) { + Object callerIdentity, AbstractEventLogger logger, boolean clientIsUsingV2) { + super(clientIsUsingV2); mStreamType = streamType; mVolume = volume; mPan = pan; @@ -76,28 +76,25 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { mDispatcher = dispatcher; mCallerIdentity = callerIdentity; mLogger = logger; + mStatusCode = TextToSpeechClient.Status.SUCCESS; } @Override void stop() { - stopImpl(false); - } - - void stopImpl(boolean wasError) { if (DBG) Log.d(TAG, "stop()"); - // Note that mLogger.mError might be true too at this point. - mLogger.onStopped(); - SynthesisPlaybackQueueItem item; synchronized (mStateLock) { - if (mStopped) { + if (mDone) { + return; + } + if (mStatusCode == TextToSpeechClient.Status.STOPPED) { Log.w(TAG, "stop() called twice"); return; } item = mItem; - mStopped = true; + mStatusCode = TextToSpeechClient.Status.STOPPED; } if (item != null) { @@ -105,19 +102,15 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { // point it will write an additional buffer to the item - but we // won't worry about that because the audio playback queue will be cleared // soon after (see SynthHandler#stop(String). - item.stop(wasError); + item.stop(TextToSpeechClient.Status.STOPPED); } else { // This happens when stop() or error() were called before start() was. // In all other cases, mAudioTrackHandler.stop() will // result in onSynthesisDone being called, and we will // write data there. - mLogger.onWriteData(); - - if (wasError) { - // We have to dispatch the error ourselves. - mDispatcher.dispatchOnError(); - } + mLogger.onCompleted(TextToSpeechClient.Status.STOPPED); + mDispatcher.dispatchOnStop(); } } @@ -129,26 +122,42 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { } @Override - boolean isDone() { - return mDone; + public boolean hasStarted() { + synchronized (mStateLock) { + return mItem != null; + } } @Override - public int start(int sampleRateInHz, int audioFormat, int channelCount) { - if (DBG) { - Log.d(TAG, "start(" + sampleRateInHz + "," + audioFormat - + "," + channelCount + ")"); + public boolean hasFinished() { + synchronized (mStateLock) { + return mDone; } + } + + @Override + public int start(int sampleRateInHz, int audioFormat, int channelCount) { + if (DBG) Log.d(TAG, "start(" + sampleRateInHz + "," + audioFormat + "," + channelCount + + ")"); int channelConfig = BlockingAudioTrack.getChannelConfig(channelCount); - if (channelConfig == 0) { - Log.e(TAG, "Unsupported number of channels :" + channelCount); - return TextToSpeech.ERROR; - } synchronized (mStateLock) { - if (mStopped) { + if (channelConfig == 0) { + Log.e(TAG, "Unsupported number of channels :" + channelCount); + mStatusCode = TextToSpeechClient.Status.ERROR_OUTPUT; + return TextToSpeech.ERROR; + } + if (mStatusCode == TextToSpeechClient.Status.STOPPED) { if (DBG) Log.d(TAG, "stop() called before start(), returning."); + return errorCodeOnStop(); + } + if (mStatusCode != TextToSpeechClient.Status.SUCCESS) { + if (DBG) Log.d(TAG, "Error was raised"); + return TextToSpeech.ERROR; + } + if (mItem != null) { + Log.e(TAG, "Start called twice"); return TextToSpeech.ERROR; } SynthesisPlaybackQueueItem item = new SynthesisPlaybackQueueItem( @@ -161,13 +170,11 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { return TextToSpeech.SUCCESS; } - @Override public int audioAvailable(byte[] buffer, int offset, int length) { - if (DBG) { - Log.d(TAG, "audioAvailable(byte[" + buffer.length + "]," - + offset + "," + length + ")"); - } + if (DBG) Log.d(TAG, "audioAvailable(byte[" + buffer.length + "]," + offset + "," + length + + ")"); + if (length > getMaxBufferSize() || length <= 0) { throw new IllegalArgumentException("buffer is too large or of zero length (" + + length + " bytes)"); @@ -175,9 +182,17 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { SynthesisPlaybackQueueItem item = null; synchronized (mStateLock) { - if (mItem == null || mStopped) { + if (mItem == null) { + mStatusCode = TextToSpeechClient.Status.ERROR_OUTPUT; return TextToSpeech.ERROR; } + if (mStatusCode != TextToSpeechClient.Status.SUCCESS) { + if (DBG) Log.d(TAG, "Error was raised"); + return TextToSpeech.ERROR; + } + if (mStatusCode == TextToSpeechClient.Status.STOPPED) { + return errorCodeOnStop(); + } item = mItem; } @@ -190,11 +205,13 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { try { item.put(bufferCopy); } catch (InterruptedException ie) { - return TextToSpeech.ERROR; + synchronized (mStateLock) { + mStatusCode = TextToSpeechClient.Status.ERROR_OUTPUT; + return TextToSpeech.ERROR; + } } mLogger.onEngineDataReceived(); - return TextToSpeech.SUCCESS; } @@ -202,35 +219,74 @@ class PlaybackSynthesisCallback extends AbstractSynthesisCallback { public int done() { if (DBG) Log.d(TAG, "done()"); + int statusCode = 0; SynthesisPlaybackQueueItem item = null; synchronized (mStateLock) { if (mDone) { Log.w(TAG, "Duplicate call to done()"); + // Not an error that would prevent synthesis. Hence no + // setStatusCode return TextToSpeech.ERROR; } - + if (mStatusCode == TextToSpeechClient.Status.STOPPED) { + if (DBG) Log.d(TAG, "Request has been aborted."); + return errorCodeOnStop(); + } mDone = true; if (mItem == null) { + // .done() was called before .start. Treat it as successful synthesis + // for a client, despite service bad implementation. + Log.w(TAG, "done() was called before start() call"); + if (mStatusCode == TextToSpeechClient.Status.SUCCESS) { + mDispatcher.dispatchOnSuccess(); + } else { + mDispatcher.dispatchOnError(mStatusCode); + } + mLogger.onEngineComplete(); return TextToSpeech.ERROR; } item = mItem; + statusCode = mStatusCode; } - item.done(); + // Signal done or error to item + if (statusCode == TextToSpeechClient.Status.SUCCESS) { + item.done(); + } else { + item.stop(statusCode); + } mLogger.onEngineComplete(); - return TextToSpeech.SUCCESS; } @Override public void error() { + error(TextToSpeechClient.Status.ERROR_SYNTHESIS); + } + + @Override + public void error(int errorCode) { if (DBG) Log.d(TAG, "error() [will call stop]"); - // Currently, this call will not be logged if error( ) is called - // before start. - mLogger.onError(); - stopImpl(true); + synchronized (mStateLock) { + if (mDone) { + return; + } + mStatusCode = errorCode; + } } + @Override + public int fallback() { + synchronized (mStateLock) { + if (hasStarted() || hasFinished()) { + return TextToSpeech.ERROR; + } + + mDispatcher.dispatchOnFallback(); + mStatusCode = TextToSpeechClient.Status.SUCCESS; + return TextToSpeechClient.Status.SUCCESS; + } + } } diff --git a/core/java/android/speech/tts/RequestConfig.java b/core/java/android/speech/tts/RequestConfig.java new file mode 100644 index 0000000..4b5385f --- /dev/null +++ b/core/java/android/speech/tts/RequestConfig.java @@ -0,0 +1,213 @@ +package android.speech.tts; + +import android.media.AudioManager; +import android.os.Bundle; + +/** + * Synthesis request configuration. + * + * This class is immutable, and can only be constructed using + * {@link RequestConfig.Builder}. + */ +public final class RequestConfig { + + /** Builder for constructing RequestConfig objects. */ + public static final class Builder { + private VoiceInfo mCurrentVoiceInfo; + private Bundle mVoiceParams; + private Bundle mAudioParams; + + Builder(VoiceInfo currentVoiceInfo, Bundle voiceParams, Bundle audioParams) { + mCurrentVoiceInfo = currentVoiceInfo; + mVoiceParams = voiceParams; + mAudioParams = audioParams; + } + + /** + * Create new RequestConfig builder. + */ + public static Builder newBuilder() { + return new Builder(null, new Bundle(), new Bundle()); + } + + /** + * Create new RequestConfig builder. + * @param prototype + * Prototype of new RequestConfig. Copies all fields of the + * prototype to the constructed object. + */ + public static Builder newBuilder(RequestConfig prototype) { + return new Builder(prototype.mCurrentVoiceInfo, + (Bundle)prototype.mVoiceParams.clone(), + (Bundle)prototype.mAudioParams.clone()); + } + + /** Set voice for request. Will reset voice parameters to the defaults. */ + public Builder setVoice(VoiceInfo voice) { + mCurrentVoiceInfo = voice; + mVoiceParams = (Bundle)voice.getParamsWithDefaults().clone(); + return this; + } + + /** + * Set request voice parameter. + * + * @param paramName + * The name of the parameter. It has to be one of the keys + * from {@link VoiceInfo#getParamsWithDefaults()} + * @param value + * Value of the parameter. Its type can be one of: Integer, Float, + * Boolean, String, VoiceInfo (will be set as a String, result of a call to + * the {@link VoiceInfo#getName()}) or byte[]. It has to be of the same type + * as the default value from {@link VoiceInfo#getParamsWithDefaults()} + * for that parameter. + * @throws IllegalArgumentException + * If paramName is not a valid parameter name or its value is of a wrong + * type. + * @throws IllegalStateException + * If no voice is set. + */ + public Builder setVoiceParam(String paramName, Object value){ + if (mCurrentVoiceInfo == null) { + throw new IllegalStateException( + "Couldn't set voice parameter, no voice is set"); + } + Object defaultValue = mCurrentVoiceInfo.getParamsWithDefaults().get(paramName); + if (defaultValue == null) { + throw new IllegalArgumentException( + "Parameter \"" + paramName + "\" is not available in set voice with " + + "name: " + mCurrentVoiceInfo.getName()); + } + + // If it's VoiceInfo, get its name + if (value instanceof VoiceInfo) { + value = ((VoiceInfo)value).getName(); + } + + // Check type information + if (!defaultValue.getClass().equals(value.getClass())) { + throw new IllegalArgumentException( + "Parameter \"" + paramName +"\" is of different type. Value passed has " + + "type " + value.getClass().getSimpleName() + " but should have " + + "type " + defaultValue.getClass().getSimpleName()); + } + + setParam(mVoiceParams, paramName, value); + return this; + } + + /** + * Set request audio parameter. + * + * Doesn't requires a set voice. + * + * @param paramName + * Name of parameter. + * @param value + * Value of parameter. Its type can be one of: Integer, Float, Boolean, String + * or byte[]. + */ + public Builder setAudioParam(String paramName, Object value) { + setParam(mAudioParams, paramName, value); + return this; + } + + /** + * Set the {@link TextToSpeechClient.Params#AUDIO_PARAM_STREAM} audio parameter. + * + * @param streamId One of the STREAM_ constants defined in {@link AudioManager}. + */ + public void setAudioParamStream(int streamId) { + setAudioParam(TextToSpeechClient.Params.AUDIO_PARAM_STREAM, streamId); + } + + /** + * Set the {@link TextToSpeechClient.Params#AUDIO_PARAM_VOLUME} audio parameter. + * + * @param volume Float in range of 0.0 to 1.0. + */ + public void setAudioParamVolume(float volume) { + setAudioParam(TextToSpeechClient.Params.AUDIO_PARAM_VOLUME, volume); + } + + /** + * Set the {@link TextToSpeechClient.Params#AUDIO_PARAM_PAN} audio parameter. + * + * @param pan Float in range of -1.0 to +1.0. + */ + public void setAudioParamPan(float pan) { + setAudioParam(TextToSpeechClient.Params.AUDIO_PARAM_PAN, pan); + } + + private void setParam(Bundle bundle, String featureName, Object value) { + if (value instanceof String) { + bundle.putString(featureName, (String)value); + } else if(value instanceof byte[]) { + bundle.putByteArray(featureName, (byte[])value); + } else if(value instanceof Integer) { + bundle.putInt(featureName, (Integer)value); + } else if(value instanceof Float) { + bundle.putFloat(featureName, (Float)value); + } else if(value instanceof Double) { + bundle.putFloat(featureName, (Float)value); + } else if(value instanceof Boolean) { + bundle.putBoolean(featureName, (Boolean)value); + } else { + throw new IllegalArgumentException("Illegal type of object"); + } + return; + } + + /** + * Build new RequestConfig instance. + */ + public RequestConfig build() { + RequestConfig config = + new RequestConfig(mCurrentVoiceInfo, mVoiceParams, mAudioParams); + return config; + } + } + + private RequestConfig(VoiceInfo voiceInfo, Bundle voiceParams, Bundle audioParams) { + mCurrentVoiceInfo = voiceInfo; + mVoiceParams = voiceParams; + mAudioParams = audioParams; + } + + /** + * Currently set voice. + */ + private final VoiceInfo mCurrentVoiceInfo; + + /** + * Voice parameters bundle. + */ + private final Bundle mVoiceParams; + + /** + * Audio parameters bundle. + */ + private final Bundle mAudioParams; + + /** + * @return Currently set request voice. + */ + public VoiceInfo getVoice() { + return mCurrentVoiceInfo; + } + + /** + * @return Request audio parameters. + */ + public Bundle getAudioParams() { + return mAudioParams; + } + + /** + * @return Request voice parameters. + */ + public Bundle getVoiceParams() { + return mVoiceParams; + } + +} diff --git a/core/java/android/speech/tts/RequestConfigHelper.java b/core/java/android/speech/tts/RequestConfigHelper.java new file mode 100644 index 0000000..b25c985 --- /dev/null +++ b/core/java/android/speech/tts/RequestConfigHelper.java @@ -0,0 +1,170 @@ +package android.speech.tts; + +import android.speech.tts.TextToSpeechClient.EngineStatus; + +import java.util.Locale; + +/** + * Set of common heuristics for selecting {@link VoiceInfo} from + * {@link TextToSpeechClient#getEngineStatus()} output. + */ +public final class RequestConfigHelper { + private RequestConfigHelper() {} + + /** + * Interface for scoring VoiceInfo object. + */ + public static interface VoiceScorer { + /** + * Score VoiceInfo. If the score is less than or equal to zero, that voice is discarded. + * If two voices have same desired primary characteristics (highest quality, lowest + * latency or others), the one with the higher score is selected. + */ + public int scoreVoice(VoiceInfo voiceInfo); + } + + /** + * Score positively voices that exactly match the locale supplied to the constructor. + */ + public static final class ExactLocaleMatcher implements VoiceScorer { + private final Locale mLocale; + + /** + * Score positively voices that exactly match the given locale + * @param locale Reference locale. If null, the default locale will be used. + */ + public ExactLocaleMatcher(Locale locale) { + if (locale == null) { + mLocale = Locale.getDefault(); + } else { + mLocale = locale; + } + } + @Override + public int scoreVoice(VoiceInfo voiceInfo) { + return mLocale.equals(voiceInfo.getLocale()) ? 1 : 0; + } + } + + /** + * Score positively voices that match exactly the given locale (score 3) + * or that share same language and country (score 2), or that share just a language (score 1). + */ + public static final class LanguageMatcher implements VoiceScorer { + private final Locale mLocale; + + /** + * Score positively voices with similar locale. + * @param locale Reference locale. If null, default will be used. + */ + public LanguageMatcher(Locale locale) { + if (locale == null) { + mLocale = Locale.getDefault(); + } else { + mLocale = locale; + } + } + + @Override + public int scoreVoice(VoiceInfo voiceInfo) { + final Locale voiceLocale = voiceInfo.getLocale(); + if (mLocale.equals(voiceLocale)) { + return 3; + } else { + if (mLocale.getLanguage().equals(voiceLocale.getLanguage())) { + if (mLocale.getCountry().equals(voiceLocale.getCountry())) { + return 2; + } + return 1; + } + return 0; + } + } + } + + /** + * Get the highest quality voice from voices that score more than zero from the passed scorer. + * If there is more than one voice with the same highest quality, then this method returns one + * with the highest score. If they share same score as well, one with the lower index in the + * voices list is returned. + * + * @param engineStatus + * Voices status received from a {@link TextToSpeechClient#getEngineStatus()} call. + * @param voiceScorer + * Used to discard unsuitable voices and help settle cases where more than + * one voice has the desired characteristic. + * @param hasToBeEmbedded + * If true, require the voice to be an embedded voice (no network + * access will be required for synthesis). + */ + private static VoiceInfo getHighestQualityVoice(EngineStatus engineStatus, + VoiceScorer voiceScorer, boolean hasToBeEmbedded) { + VoiceInfo bestVoice = null; + int bestScoreMatch = 1; + int bestVoiceQuality = 0; + + for (VoiceInfo voice : engineStatus.getVoices()) { + int score = voiceScorer.scoreVoice(voice); + if (score <= 0 || hasToBeEmbedded && voice.getRequiresNetworkConnection() + || voice.getQuality() < bestVoiceQuality) { + continue; + } + + if (bestVoice == null || + voice.getQuality() > bestVoiceQuality || + score > bestScoreMatch) { + bestVoice = voice; + bestScoreMatch = score; + bestVoiceQuality = voice.getQuality(); + } + } + return bestVoice; + } + + /** + * Get highest quality voice. + * + * Highest quality voice is selected from voices that score more than zero from the passed + * scorer. If there is more than one voice with the same highest quality, then this method + * will return one with the highest score. If they share same score as well, one with the lower + * index in the voices list is returned. + + * @param engineStatus + * Voices status received from a {@link TextToSpeechClient#getEngineStatus()} call. + * @param hasToBeEmbedded + * If true, require the voice to be an embedded voice (no network + * access will be required for synthesis). + * @param voiceScorer + * Scorer is used to discard unsuitable voices and help settle cases where more than + * one voice has highest quality. + * @return RequestConfig with selected voice or null if suitable voice was not found. + */ + public static RequestConfig highestQuality(EngineStatus engineStatus, + boolean hasToBeEmbedded, VoiceScorer voiceScorer) { + VoiceInfo voice = getHighestQualityVoice(engineStatus, voiceScorer, hasToBeEmbedded); + if (voice == null) { + return null; + } + return RequestConfig.Builder.newBuilder().setVoice(voice).build(); + } + + /** + * Get highest quality voice for the default locale. + * + * Call {@link #highestQuality(EngineStatus, boolean, VoiceScorer)} with + * {@link LanguageMatcher} set to device default locale. + * + * @param engineStatus + * Voices status received from a {@link TextToSpeechClient#getEngineStatus()} call. + * @param hasToBeEmbedded + * If true, require the voice to be an embedded voice (no network + * access will be required for synthesis). + * @return RequestConfig with selected voice or null if suitable voice was not found. + */ + public static RequestConfig highestQuality(EngineStatus engineStatus, + boolean hasToBeEmbedded) { + return highestQuality(engineStatus, hasToBeEmbedded, + new LanguageMatcher(Locale.getDefault())); + } + +} diff --git a/core/java/android/speech/tts/SilencePlaybackQueueItem.java b/core/java/android/speech/tts/SilencePlaybackQueueItem.java index a5e47ae..88b7c70 100644 --- a/core/java/android/speech/tts/SilencePlaybackQueueItem.java +++ b/core/java/android/speech/tts/SilencePlaybackQueueItem.java @@ -17,7 +17,6 @@ package android.speech.tts; import android.os.ConditionVariable; import android.speech.tts.TextToSpeechService.UtteranceProgressDispatcher; -import android.util.Log; class SilencePlaybackQueueItem extends PlaybackQueueItem { private final ConditionVariable mCondVar = new ConditionVariable(); @@ -32,14 +31,20 @@ class SilencePlaybackQueueItem extends PlaybackQueueItem { @Override public void run() { getDispatcher().dispatchOnStart(); + boolean wasStopped = false; if (mSilenceDurationMs > 0) { - mCondVar.block(mSilenceDurationMs); + wasStopped = mCondVar.block(mSilenceDurationMs); } - getDispatcher().dispatchOnDone(); + if (wasStopped) { + getDispatcher().dispatchOnStop(); + } else { + getDispatcher().dispatchOnSuccess(); + } + } @Override - void stop(boolean isError) { + void stop(int errorCode) { mCondVar.open(); } } diff --git a/core/java/android/speech/tts/SynthesisCallback.java b/core/java/android/speech/tts/SynthesisCallback.java index f98bb09..bc2f239 100644 --- a/core/java/android/speech/tts/SynthesisCallback.java +++ b/core/java/android/speech/tts/SynthesisCallback.java @@ -26,7 +26,9 @@ package android.speech.tts; * indicate that an error has occurred, but if the call is made after a call * to {@link #done}, it might be discarded. * - * After {@link #start} been called, {@link #done} must be called regardless of errors. + * {@link #done} must be called at the end of synthesis, regardless of errors. + * + * All methods can be only called on the synthesis thread. */ public interface SynthesisCallback { /** @@ -41,13 +43,16 @@ public interface SynthesisCallback { * request. * * This method should only be called on the synthesis thread, - * while in {@link TextToSpeechService#onSynthesizeText}. + * while in {@link TextToSpeechService#onSynthesizeText} or + * {@link TextToSpeechService#onSynthesizeTextV2}. * * @param sampleRateInHz Sample rate in HZ of the generated audio. * @param audioFormat Audio format of the generated audio. Must be one of * the ENCODING_ constants defined in {@link android.media.AudioFormat}. * @param channelCount The number of channels. Must be {@code 1} or {@code 2}. - * @return {@link TextToSpeech#SUCCESS} or {@link TextToSpeech#ERROR}. + * @return {@link TextToSpeech#SUCCESS}, {@link TextToSpeech#ERROR}. + * {@link TextToSpeechClient.Status#STOPPED} is also possible if called in context of + * {@link TextToSpeechService#onSynthesizeTextV2}. */ public int start(int sampleRateInHz, int audioFormat, int channelCount); @@ -55,7 +60,8 @@ public interface SynthesisCallback { * The service should call this method when synthesized audio is ready for consumption. * * This method should only be called on the synthesis thread, - * while in {@link TextToSpeechService#onSynthesizeText}. + * while in {@link TextToSpeechService#onSynthesizeText} or + * {@link TextToSpeechService#onSynthesizeTextV2}. * * @param buffer The generated audio data. This method will not hold on to {@code buffer}, * so the caller is free to modify it after this method returns. @@ -63,6 +69,8 @@ public interface SynthesisCallback { * @param length The number of bytes of audio data in {@code buffer}. This must be * less than or equal to the return value of {@link #getMaxBufferSize}. * @return {@link TextToSpeech#SUCCESS} or {@link TextToSpeech#ERROR}. + * {@link TextToSpeechClient.Status#STOPPED} is also possible if called in context of + * {@link TextToSpeechService#onSynthesizeTextV2}. */ public int audioAvailable(byte[] buffer, int offset, int length); @@ -71,11 +79,14 @@ public interface SynthesisCallback { * been passed to {@link #audioAvailable}. * * This method should only be called on the synthesis thread, - * while in {@link TextToSpeechService#onSynthesizeText}. + * while in {@link TextToSpeechService#onSynthesizeText} or + * {@link TextToSpeechService#onSynthesizeTextV2}. * - * This method has to be called if {@link #start} was called. + * This method has to be called if {@link #start} and/or {@link #error} was called. * * @return {@link TextToSpeech#SUCCESS} or {@link TextToSpeech#ERROR}. + * {@link TextToSpeechClient.Status#STOPPED} is also possible if called in context of + * {@link TextToSpeechService#onSynthesizeTextV2}. */ public int done(); @@ -87,4 +98,58 @@ public interface SynthesisCallback { */ public void error(); -}
\ No newline at end of file + + /** + * The service should call this method if the speech synthesis fails. + * + * This method should only be called on the synthesis thread, + * while in {@link TextToSpeechService#onSynthesizeText} or + * {@link TextToSpeechService#onSynthesizeTextV2}. + * + * @param errorCode Error code to pass to the client. One of the ERROR_ values from + * {@link TextToSpeechClient.Status} + */ + public void error(int errorCode); + + /** + * Communicate to client that the original request can't be done and client-requested + * fallback is happening. + * + * Fallback can be requested by the client by setting + * {@link TextToSpeechClient.Params#FALLBACK_VOICE_NAME} voice parameter with a id of + * the voice that is expected to be used for the fallback. + * + * This method will fail if user called {@link #start(int, int, int)} and/or + * {@link #done()}. + * + * This method should only be called on the synthesis thread, + * while in {@link TextToSpeechService#onSynthesizeTextV2}. + * + * @return {@link TextToSpeech#SUCCESS}, {@link TextToSpeech#ERROR} if client already + * called {@link #start(int, int, int)}, {@link TextToSpeechClient.Status#STOPPED} + * if stop was requested. + */ + public int fallback(); + + /** + * Check if {@link #start} was called or not. + * + * This method should only be called on the synthesis thread, + * while in {@link TextToSpeechService#onSynthesizeText} or + * {@link TextToSpeechService#onSynthesizeTextV2}. + * + * Useful for checking if a fallback from network request is possible. + */ + public boolean hasStarted(); + + /** + * Check if {@link #done} was called or not. + * + * This method should only be called on the synthesis thread, + * while in {@link TextToSpeechService#onSynthesizeText} or + * {@link TextToSpeechService#onSynthesizeTextV2}. + * + * Useful for checking if a fallback from network request is possible. + */ + public boolean hasFinished(); +} diff --git a/core/java/android/speech/tts/SynthesisPlaybackQueueItem.java b/core/java/android/speech/tts/SynthesisPlaybackQueueItem.java index e853c9e..b424356 100644 --- a/core/java/android/speech/tts/SynthesisPlaybackQueueItem.java +++ b/core/java/android/speech/tts/SynthesisPlaybackQueueItem.java @@ -57,23 +57,22 @@ final class SynthesisPlaybackQueueItem extends PlaybackQueueItem { */ private volatile boolean mStopped; private volatile boolean mDone; - private volatile boolean mIsError; + private volatile int mStatusCode; private final BlockingAudioTrack mAudioTrack; - private final EventLogger mLogger; - + private final AbstractEventLogger mLogger; SynthesisPlaybackQueueItem(int streamType, int sampleRate, int audioFormat, int channelCount, float volume, float pan, UtteranceProgressDispatcher dispatcher, - Object callerIdentity, EventLogger logger) { + Object callerIdentity, AbstractEventLogger logger) { super(dispatcher, callerIdentity); mUnconsumedBytes = 0; mStopped = false; mDone = false; - mIsError = false; + mStatusCode = TextToSpeechClient.Status.SUCCESS; mAudioTrack = new BlockingAudioTrack(streamType, sampleRate, audioFormat, channelCount, volume, pan); @@ -86,9 +85,8 @@ final class SynthesisPlaybackQueueItem extends PlaybackQueueItem { final UtteranceProgressDispatcher dispatcher = getDispatcher(); dispatcher.dispatchOnStart(); - if (!mAudioTrack.init()) { - dispatcher.dispatchOnError(); + dispatcher.dispatchOnError(TextToSpeechClient.Status.ERROR_OUTPUT); return; } @@ -112,23 +110,25 @@ final class SynthesisPlaybackQueueItem extends PlaybackQueueItem { mAudioTrack.waitAndRelease(); - if (mIsError) { - dispatcher.dispatchOnError(); + if (mStatusCode == TextToSpeechClient.Status.SUCCESS) { + dispatcher.dispatchOnSuccess(); + } else if(mStatusCode == TextToSpeechClient.Status.STOPPED) { + dispatcher.dispatchOnStop(); } else { - dispatcher.dispatchOnDone(); + dispatcher.dispatchOnError(mStatusCode); } - mLogger.onWriteData(); + mLogger.onCompleted(mStatusCode); } @Override - void stop(boolean isError) { + void stop(int statusCode) { try { mListLock.lock(); // Update our internal state. mStopped = true; - mIsError = isError; + mStatusCode = statusCode; // Wake up the audio playback thread if it was waiting on take(). // take() will return null since mStopped was true, and will then diff --git a/core/java/android/speech/tts/SynthesisRequestV2.aidl b/core/java/android/speech/tts/SynthesisRequestV2.aidl new file mode 100644 index 0000000..2ac7da6 --- /dev/null +++ b/core/java/android/speech/tts/SynthesisRequestV2.aidl @@ -0,0 +1,20 @@ +/* +** +** Copyright 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.speech.tts; + +parcelable SynthesisRequestV2;
\ No newline at end of file diff --git a/core/java/android/speech/tts/SynthesisRequestV2.java b/core/java/android/speech/tts/SynthesisRequestV2.java new file mode 100644 index 0000000..a1da49c --- /dev/null +++ b/core/java/android/speech/tts/SynthesisRequestV2.java @@ -0,0 +1,144 @@ +package android.speech.tts; + +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.speech.tts.TextToSpeechClient.UtteranceId; + +/** + * Service-side representation of a synthesis request from a V2 API client. Contains: + * <ul> + * <li>The utterance to synthesize</li> + * <li>The id of the utterance (String, result of {@link UtteranceId#toUniqueString()}</li> + * <li>The synthesis voice name (String, result of {@link VoiceInfo#getName()})</li> + * <li>Voice parameters (Bundle of parameters)</li> + * <li>Audio parameters (Bundle of parameters)</li> + * </ul> + */ +public final class SynthesisRequestV2 implements Parcelable { + /** Synthesis utterance. */ + private final String mText; + + /** Synthesis id. */ + private final String mUtteranceId; + + /** Voice ID. */ + private final String mVoiceName; + + /** Voice Parameters. */ + private final Bundle mVoiceParams; + + /** Audio Parameters. */ + private final Bundle mAudioParams; + + /** + * Constructor for test purposes. + */ + public SynthesisRequestV2(String text, String utteranceId, String voiceName, + Bundle voiceParams, Bundle audioParams) { + this.mText = text; + this.mUtteranceId = utteranceId; + this.mVoiceName = voiceName; + this.mVoiceParams = voiceParams; + this.mAudioParams = audioParams; + } + + /** + * Parcel based constructor. + * + * @hide + */ + public SynthesisRequestV2(Parcel in) { + this.mText = in.readString(); + this.mUtteranceId = in.readString(); + this.mVoiceName = in.readString(); + this.mVoiceParams = in.readBundle(); + this.mAudioParams = in.readBundle(); + } + + SynthesisRequestV2(String text, String utteranceId, RequestConfig rconfig) { + this.mText = text; + this.mUtteranceId = utteranceId; + this.mVoiceName = rconfig.getVoice().getName(); + this.mVoiceParams = rconfig.getVoiceParams(); + this.mAudioParams = rconfig.getAudioParams(); + } + + /** + * Write to parcel. + * + * @hide + */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mText); + dest.writeString(mUtteranceId); + dest.writeString(mVoiceName); + dest.writeBundle(mVoiceParams); + dest.writeBundle(mAudioParams); + } + + /** + * @return the text which should be synthesized. + */ + public String getText() { + return mText; + } + + /** + * @return the id of the synthesis request. It's an output of a call to the + * {@link UtteranceId#toUniqueString()} method of the {@link UtteranceId} associated with + * this request. + */ + public String getUtteranceId() { + return mUtteranceId; + } + + /** + * @return the name of the voice to use for this synthesis request. Result of a call to + * the {@link VoiceInfo#getName()} method. + */ + public String getVoiceName() { + return mVoiceName; + } + + /** + * @return bundle of voice parameters. + */ + public Bundle getVoiceParams() { + return mVoiceParams; + } + + /** + * @return bundle of audio parameters. + */ + public Bundle getAudioParams() { + return mAudioParams; + } + + /** + * Parcel creators. + * + * @hide + */ + public static final Parcelable.Creator<SynthesisRequestV2> CREATOR = + new Parcelable.Creator<SynthesisRequestV2>() { + @Override + public SynthesisRequestV2 createFromParcel(Parcel source) { + return new SynthesisRequestV2(source); + } + + @Override + public SynthesisRequestV2[] newArray(int size) { + return new SynthesisRequestV2[size]; + } + }; + + /** + * @hide + */ + @Override + public int describeContents() { + return 0; + } +} diff --git a/core/java/android/speech/tts/TextToSpeech.java b/core/java/android/speech/tts/TextToSpeech.java index 2752085..c527acf 100644 --- a/core/java/android/speech/tts/TextToSpeech.java +++ b/core/java/android/speech/tts/TextToSpeech.java @@ -54,7 +54,9 @@ import java.util.Set; * When you are done using the TextToSpeech instance, call the {@link #shutdown()} method * to release the native resources used by the TextToSpeech engine. * + * @deprecated Use {@link TextToSpeechClient} instead */ +@Deprecated public class TextToSpeech { private static final String TAG = "TextToSpeech"; @@ -970,7 +972,7 @@ public class TextToSpeech { @Override public Integer run(ITextToSpeechService service) throws RemoteException { return service.playSilence(getCallerIdentity(), durationInMs, queueMode, - getParams(params)); + params == null ? null : params.get(Engine.KEY_PARAM_UTTERANCE_ID)); } }, ERROR, "playSilence"); } @@ -988,6 +990,7 @@ public class TextToSpeech { * must be behave as per {@link Boolean#parseBoolean(String)}. * * @param locale The locale to query features for. + * @return Set instance. May return {@code null} on error. */ public Set<String> getFeatures(final Locale locale) { return runAction(new Action<Set<String>>() { @@ -1443,8 +1446,17 @@ public class TextToSpeech { private boolean mEstablished; private final ITextToSpeechCallback.Stub mCallback = new ITextToSpeechCallback.Stub() { + public void onStop(String utteranceId) throws RemoteException { + // do nothing + }; + + @Override + public void onFallback(String utteranceId) throws RemoteException { + // do nothing + } + @Override - public void onDone(String utteranceId) { + public void onSuccess(String utteranceId) { UtteranceProgressListener listener = mUtteranceProgressListener; if (listener != null) { listener.onDone(utteranceId); @@ -1452,7 +1464,7 @@ public class TextToSpeech { } @Override - public void onError(String utteranceId) { + public void onError(String utteranceId, int errorCode) { UtteranceProgressListener listener = mUtteranceProgressListener; if (listener != null) { listener.onError(utteranceId); @@ -1466,6 +1478,11 @@ public class TextToSpeech { listener.onStart(utteranceId); } } + + @Override + public void onVoicesInfoChange(List<VoiceInfo> voicesInfo) throws RemoteException { + // Ignore it + } }; private class SetupConnectionAsyncTask extends AsyncTask<Void, Void, Integer> { @@ -1484,11 +1501,13 @@ public class TextToSpeech { try { mService.setCallback(getCallerIdentity(), mCallback); - String[] defaultLanguage = mService.getClientDefaultLanguage(); - mParams.putString(Engine.KEY_PARAM_LANGUAGE, defaultLanguage[0]); - mParams.putString(Engine.KEY_PARAM_COUNTRY, defaultLanguage[1]); - mParams.putString(Engine.KEY_PARAM_VARIANT, defaultLanguage[2]); + if (mParams.getString(Engine.KEY_PARAM_LANGUAGE) == null) { + String[] defaultLanguage = mService.getClientDefaultLanguage(); + mParams.putString(Engine.KEY_PARAM_LANGUAGE, defaultLanguage[0]); + mParams.putString(Engine.KEY_PARAM_COUNTRY, defaultLanguage[1]); + mParams.putString(Engine.KEY_PARAM_VARIANT, defaultLanguage[2]); + } Log.i(TAG, "Set up connection to " + mName); return SUCCESS; diff --git a/core/java/android/speech/tts/TextToSpeechClient.java b/core/java/android/speech/tts/TextToSpeechClient.java new file mode 100644 index 0000000..10e2073 --- /dev/null +++ b/core/java/android/speech/tts/TextToSpeechClient.java @@ -0,0 +1,1047 @@ +/* + * 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.speech.tts; + +import android.app.Activity; +import android.app.Application; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.media.AudioManager; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.IBinder; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.speech.tts.ITextToSpeechCallback; +import android.speech.tts.ITextToSpeechService; +import android.util.Log; +import android.util.Pair; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Synthesizes speech from text for immediate playback or to create a sound + * file. + * <p> + * This is an updated version of the speech synthesis client that supersedes + * {@link android.speech.tts.TextToSpeech}. + * <p> + * A TextToSpeechClient instance can only be used to synthesize text once it has + * connected to the service. The TextToSpeechClient instance will start establishing + * the connection after a call to the {@link #connect()} method. This is usually done in + * {@link Application#onCreate()} or {@link Activity#onCreate}. When the connection + * is established, the instance will call back using the + * {@link TextToSpeechClient.ConnectionCallbacks} interface. Only after a + * successful callback is the client usable. + * <p> + * After successful connection, the list of all available voices can be obtained + * by calling the {@link TextToSpeechClient#getEngineStatus()} method. The client can + * choose a voice using some custom heuristic and build a {@link RequestConfig} object + * using {@link RequestConfig.Builder}, or can use one of the common heuristics found + * in ({@link RequestConfigHelper}. + * <p> + * When you are done using the TextToSpeechClient instance, call the + * {@link #disconnect()} method to release the connection. + * <p> + * In the rare case of a change to the set of available voices, the service will call to the + * {@link ConnectionCallbacks#onEngineStatusChange} with new set of available voices as argument. + * In response, the client HAVE to recreate all {@link RequestConfig} instances in use. + */ +public class TextToSpeechClient { + private static final String TAG = TextToSpeechClient.class.getSimpleName(); + + private final Object mLock = new Object(); + private final TtsEngines mEnginesHelper; + private final Context mContext; + + // Guarded by mLock + private Connection mServiceConnection; + private final RequestCallbacks mDefaultRequestCallbacks; + private final ConnectionCallbacks mConnectionCallbacks; + private EngineStatus mEngineStatus; + private String mRequestedEngine; + private boolean mFallbackToDefault; + private HashMap<String, Pair<UtteranceId, RequestCallbacks>> mCallbacks; + // Guarded by mLock + + /** Common voices parameters */ + public static final class Params { + private Params() {} + + /** + * Maximum allowed time for a single request attempt, in milliseconds, before synthesis + * fails (or fallback request starts, if requested using + * {@link #FALLBACK_VOICE_NAME}). + */ + public static final String NETWORK_TIMEOUT_MS = "networkTimeoutMs"; + + /** + * Number of network request retries that are attempted in case of failure + */ + public static final String NETWORK_RETRIES_COUNT = "networkRetriesCount"; + + /** + * Should synthesizer report sub-utterance progress on synthesis. Only applicable + * for the {@link TextToSpeechClient#queueSpeak} method. + */ + public static final String TRACK_SUBUTTERANCE_PROGRESS = "trackSubutteranceProgress"; + + /** + * If a voice exposes this parameter then it supports the fallback request feature. + * + * If it is set to a valid name of some other voice ({@link VoiceInfo#getName()}) then + * in case of request failure (due to network problems or missing data), fallback request + * will be attempted. Request will be done using the voice referenced by this parameter. + * If it is the case, the client will be informed by a callback to the {@link + * RequestCallbacks#onSynthesisFallback(UtteranceId)}. + */ + public static final String FALLBACK_VOICE_NAME = "fallbackVoiceName"; + + /** + * Audio parameter for specifying a linear multiplier to the speaking speed of the voice. + * The value is a float. Values below zero decrease speed of the synthesized speech + * values above one increase it. If the value of this parameter is equal to zero, + * then it will be replaced by a settings-configurable default before it reaches + * TTS service. + */ + public static final String SPEECH_SPEED = "speechSpeed"; + + /** + * Audio parameter for controlling the pitch of the output. The Value is a positive float, + * with default of {@code 1.0}. The value is used to scale the primary frequency linearly. + * Lower values lower the tone of the synthesized voice, greater values increase it. + */ + public static final String SPEECH_PITCH = "speechPitch"; + + /** + * Audio parameter for controlling output volume. Value is a float with scale of 0 to 1 + */ + public static final String AUDIO_PARAM_VOLUME = TextToSpeech.Engine.KEY_PARAM_VOLUME; + + /** + * Audio parameter for controlling output pan. + * Value is a float ranging from -1 to +1 where -1 maps to a hard-left pan, + * 0 to center (the default behavior), and +1 to hard-right. + */ + public static final String AUDIO_PARAM_PAN = TextToSpeech.Engine.KEY_PARAM_PAN; + + /** + * Audio parameter for specifying the audio stream type to be used when speaking text + * or playing back a file. The value should be one of the STREAM_ constants + * defined in {@link AudioManager}. + */ + public static final String AUDIO_PARAM_STREAM = TextToSpeech.Engine.KEY_PARAM_STREAM; + } + + /** + * Result codes for TTS operations. + */ + public static final class Status { + private Status() {} + + /** + * Denotes a successful operation. + */ + public static final int SUCCESS = 0; + + /** + * Denotes a stop requested by a client. It's used only on the service side of the API, + * client should never expect to see this result code. + */ + public static final int STOPPED = 100; + + /** + * Denotes a generic failure. + */ + public static final int ERROR_UNKNOWN = -1; + + /** + * Denotes a failure of a TTS engine to synthesize the given input. + */ + public static final int ERROR_SYNTHESIS = 10; + + /** + * Denotes a failure of a TTS service. + */ + public static final int ERROR_SERVICE = 11; + + /** + * Denotes a failure related to the output (audio device or a file). + */ + public static final int ERROR_OUTPUT = 12; + + /** + * Denotes a failure caused by a network connectivity problems. + */ + public static final int ERROR_NETWORK = 13; + + /** + * Denotes a failure caused by network timeout. + */ + public static final int ERROR_NETWORK_TIMEOUT = 14; + + /** + * Denotes a failure caused by an invalid request. + */ + public static final int ERROR_INVALID_REQUEST = 15; + + /** + * Denotes a failure related to passing a non-unique utterance id. + */ + public static final int ERROR_NON_UNIQUE_UTTERANCE_ID = 16; + + /** + * Denotes a failure related to missing data. The TTS implementation may download + * the missing data, and if so, request will succeed in future. This error can only happen + * for voices with {@link VoiceInfo#FEATURE_MAY_AUTOINSTALL} feature. + * Note: the recommended way to avoid this error is to create a request with the fallback + * voice. + */ + public static final int ERROR_DOWNLOADING_ADDITIONAL_DATA = 17; + } + + /** + * Set of callbacks for the events related to the progress of a synthesis request + * through the synthesis queue. Each synthesis request is associated with a call to + * {@link #queueSpeak} or {@link #queueSynthesizeToFile}. + * + * The callbacks specified in this method will NOT be called on UI thread. + */ + public static abstract class RequestCallbacks { + /** + * Called after synthesis of utterance successfully starts. + */ + public void onSynthesisStart(UtteranceId utteranceId) {} + + /** + * Called after synthesis successfully finishes. + * @param utteranceId + * Unique identifier of synthesized utterance. + */ + public void onSynthesisSuccess(UtteranceId utteranceId) {} + + /** + * Called after synthesis was stopped in middle of synthesis process. + * @param utteranceId + * Unique identifier of synthesized utterance. + */ + public void onSynthesisStop(UtteranceId utteranceId) {} + + /** + * Called when requested synthesis failed and fallback synthesis is about to be attempted. + * + * Requires voice with available {@link TextToSpeechClient.Params#FALLBACK_VOICE_NAME} + * parameter, and request with this parameter enabled. + * + * This callback will be followed by callback to the {@link #onSynthesisStart}, + * {@link #onSynthesisFailure} or {@link #onSynthesisSuccess} that depends on the + * fallback outcome. + * + * For more fallback feature reference, look at the + * {@link TextToSpeechClient.Params#FALLBACK_VOICE_NAME}. + * + * @param utteranceId + * Unique identifier of synthesized utterance. + */ + public void onSynthesisFallback(UtteranceId utteranceId) {} + + /** + * Called after synthesis of utterance fails. + * + * It may be called instead or after a {@link #onSynthesisStart} callback. + * + * @param utteranceId + * Unique identifier of synthesized utterance. + * @param errorCode + * One of the values from {@link Status}. + */ + public void onSynthesisFailure(UtteranceId utteranceId, int errorCode) {} + + /** + * Called during synthesis to mark synthesis progress. + * + * Requires voice with available + * {@link TextToSpeechClient.Params#TRACK_SUBUTTERANCE_PROGRESS} parameter, and + * request with this parameter enabled. + * + * @param utteranceId + * Unique identifier of synthesized utterance. + * @param charIndex + * String index (java char offset) of recently synthesized character. + * @param msFromStart + * Miliseconds from the start of the synthesis. + */ + public void onSynthesisProgress(UtteranceId utteranceId, int charIndex, + int msFromStart) {} + } + + /** + * Interface definition of callbacks that are called when the client is + * connected or disconnected from the TTS service. + */ + public static interface ConnectionCallbacks { + /** + * After calling {@link TextToSpeechClient#connect()}, this method will be invoked + * asynchronously when the connect request has successfully completed. + * + * Clients are strongly encouraged to call {@link TextToSpeechClient#getEngineStatus()} + * and create {@link RequestConfig} objects used in subsequent synthesis requests. + */ + public void onConnectionSuccess(); + + /** + * After calling {@link TextToSpeechClient#connect()}, this method may be invoked + * asynchronously when the connect request has failed to complete. + * + * It may be also invoked synchronously, from the body of + * {@link TextToSpeechClient#connect()} method. + */ + public void onConnectionFailure(); + + /** + * Called when the connection to the service is lost. This can happen if there is a problem + * with the speech service (e.g. a crash or resource problem causes it to be killed by the + * system). When called, all requests have been canceled and no outstanding listeners will + * be executed. Applications should disable UI components that require the service. + */ + public void onServiceDisconnected(); + + /** + * After receiving {@link #onConnectionSuccess()} callback, this method may be invoked + * if engine status obtained from {@link TextToSpeechClient#getEngineStatus()}) changes. + * It usually means that some voices were removed, changed or added. + * + * Clients are required to recreate {@link RequestConfig} objects used in subsequent + * synthesis requests. + */ + public void onEngineStatusChange(EngineStatus newEngineStatus); + } + + /** State of voices as provided by engine and user. */ + public static final class EngineStatus { + /** All available voices. */ + private final List<VoiceInfo> mVoices; + + /** Name of the TTS engine package */ + private final String mPackageName; + + private EngineStatus(String packageName, List<VoiceInfo> voices) { + this.mVoices = Collections.unmodifiableList(voices); + this.mPackageName = packageName; + } + + /** + * Get an immutable list of all Voices exposed by the TTS engine. + */ + public List<VoiceInfo> getVoices() { + return mVoices; + } + + /** + * Get name of the TTS engine package currently in use. + */ + public String getEnginePackage() { + return mPackageName; + } + } + + /** Unique synthesis request identifier. */ + public static class UtteranceId { + /** Unique identifier */ + private final int id; + + /** Unique identifier generator */ + private static final AtomicInteger ID_GENERATOR = new AtomicInteger(); + + /** + * Create new, unique UtteranceId instance. + */ + public UtteranceId() { + id = ID_GENERATOR.getAndIncrement(); + } + + /** + * Returns a unique string associated with an instance of this object. + * + * This string will be used to identify the synthesis request/utterance inside the + * TTS service. + */ + public final String toUniqueString() { + return "UID" + id; + } + } + + /** + * Create TextToSpeech service client. + * + * Will connect to the default TTS service. In order to be usable, {@link #connect()} need + * to be called first and successful connection callback need to be received. + * + * @param context + * The context this instance is running in. + * @param engine + * Package name of requested TTS engine. If it's null, then default engine will + * be selected regardless of {@code fallbackToDefaultEngine} parameter value. + * @param fallbackToDefaultEngine + * If requested engine is not available, should we fallback to the default engine? + * @param defaultRequestCallbacks + * Default request callbacks, it will be used for all synthesis requests without + * supplied RequestCallbacks instance. Can't be null. + * @param connectionCallbacks + * Callbacks for connecting and disconnecting from the service. Can't be null. + */ + public TextToSpeechClient(Context context, + String engine, boolean fallbackToDefaultEngine, + RequestCallbacks defaultRequestCallbacks, + ConnectionCallbacks connectionCallbacks) { + if (context == null) + throw new IllegalArgumentException("context can't be null"); + if (defaultRequestCallbacks == null) + throw new IllegalArgumentException("defaultRequestCallbacks can't be null"); + if (connectionCallbacks == null) + throw new IllegalArgumentException("connectionCallbacks can't be null"); + mContext = context; + mEnginesHelper = new TtsEngines(mContext); + mCallbacks = new HashMap<String, Pair<UtteranceId, RequestCallbacks>>(); + mDefaultRequestCallbacks = defaultRequestCallbacks; + mConnectionCallbacks = connectionCallbacks; + + mRequestedEngine = engine; + mFallbackToDefault = fallbackToDefaultEngine; + } + + /** + * Create TextToSpeech service client. Will connect to the default TTS + * service. In order to be usable, {@link #connect()} need to be called + * first and successful connection callback need to be received. + * + * @param context Context this instance is running in. + * @param defaultRequestCallbacks Default request callbacks, it + * will be used for all synthesis requests without supplied + * RequestCallbacks instance. Can't be null. + * @param connectionCallbacks Callbacks for connecting and disconnecting + * from the service. Can't be null. + */ + public TextToSpeechClient(Context context, RequestCallbacks defaultRequestCallbacks, + ConnectionCallbacks connectionCallbacks) { + this(context, null, true, defaultRequestCallbacks, connectionCallbacks); + } + + + private boolean initTts(String requestedEngine, boolean fallbackToDefaultEngine) { + // Step 1: Try connecting to the engine that was requested. + if (requestedEngine != null) { + if (mEnginesHelper.isEngineInstalled(requestedEngine)) { + if ((mServiceConnection = connectToEngine(requestedEngine)) != null) { + return true; + } else if (!fallbackToDefaultEngine) { + Log.w(TAG, "Couldn't connect to requested engine: " + requestedEngine); + return false; + } + } else if (!fallbackToDefaultEngine) { + Log.w(TAG, "Requested engine not installed: " + requestedEngine); + return false; + } + } + + // Step 2: Try connecting to the user's default engine. + final String defaultEngine = mEnginesHelper.getDefaultEngine(); + if (defaultEngine != null && !defaultEngine.equals(requestedEngine)) { + if ((mServiceConnection = connectToEngine(defaultEngine)) != null) { + return true; + } + } + + // Step 3: Try connecting to the highest ranked engine in the + // system. + final String highestRanked = mEnginesHelper.getHighestRankedEngineName(); + if (highestRanked != null && !highestRanked.equals(requestedEngine) && + !highestRanked.equals(defaultEngine)) { + if ((mServiceConnection = connectToEngine(highestRanked)) != null) { + return true; + } + } + + Log.w(TAG, "Couldn't find working TTS engine"); + return false; + } + + private Connection connectToEngine(String engine) { + Connection connection = new Connection(engine); + Intent intent = new Intent(TextToSpeech.Engine.INTENT_ACTION_TTS_SERVICE); + intent.setPackage(engine); + boolean bound = mContext.bindService(intent, connection, Context.BIND_AUTO_CREATE); + if (!bound) { + Log.e(TAG, "Failed to bind to " + engine); + return null; + } else { + Log.i(TAG, "Successfully bound to " + engine); + return connection; + } + } + + + /** + * Connects the client to TTS service. This method returns immediately, and connects to the + * service in the background. + * + * After connection initializes successfully, {@link ConnectionCallbacks#onConnectionSuccess()} + * is called. On a failure {@link ConnectionCallbacks#onConnectionFailure} is called. + * + * Both of those callback may be called asynchronously on the main thread, + * {@link ConnectionCallbacks#onConnectionFailure} may be called synchronously, before + * this method returns. + */ + public void connect() { + synchronized (mLock) { + if (mServiceConnection != null) { + return; + } + if(!initTts(mRequestedEngine, mFallbackToDefault)) { + mConnectionCallbacks.onConnectionFailure(); + } + } + } + + /** + * Checks if the client is currently connected to the service, so that + * requests to other methods will succeed. + */ + public boolean isConnected() { + synchronized (mLock) { + return mServiceConnection != null && mServiceConnection.isEstablished(); + } + } + + /** + * Closes the connection to TextToSpeech service. No calls can be made on this object after + * calling this method. + * It is good practice to call this method in the onDestroy() method of an Activity + * so the TextToSpeech engine can be cleanly stopped. + */ + public void disconnect() { + synchronized (mLock) { + if (mServiceConnection != null) { + mServiceConnection.disconnect(); + mServiceConnection = null; + mCallbacks.clear(); + } + } + } + + /** + * Register callback. + * + * @param utteranceId Non-null UtteranceId instance. + * @param callback Non-null callbacks for the request + * @return Status.SUCCESS or error code in case of invalid arguments. + */ + private int addCallback(UtteranceId utteranceId, RequestCallbacks callback) { + synchronized (mLock) { + if (utteranceId == null || callback == null) { + return Status.ERROR_INVALID_REQUEST; + } + if (mCallbacks.put(utteranceId.toUniqueString(), + new Pair<UtteranceId, RequestCallbacks>(utteranceId, callback)) != null) { + return Status.ERROR_NON_UNIQUE_UTTERANCE_ID; + } + return Status.SUCCESS; + } + } + + /** + * Remove and return callback. + * + * @param utteranceIdStr Unique string obtained from {@link UtteranceId#toUniqueString}. + */ + private Pair<UtteranceId, RequestCallbacks> removeCallback(String utteranceIdStr) { + synchronized (mLock) { + return mCallbacks.remove(utteranceIdStr); + } + } + + /** + * Get callback and utterance id. + * + * @param utteranceIdStr Unique string obtained from {@link UtteranceId#toUniqueString}. + */ + private Pair<UtteranceId, RequestCallbacks> getCallback(String utteranceIdStr) { + synchronized (mLock) { + return mCallbacks.get(utteranceIdStr); + } + } + + /** + * Remove callback and call {@link RequestCallbacks#onSynthesisFailure} with passed + * error code. + * + * @param utteranceIdStr Unique string obtained from {@link UtteranceId#toUniqueString}. + * @param errorCode argument to {@link RequestCallbacks#onSynthesisFailure} call. + */ + private void removeCallbackAndErr(String utteranceIdStr, int errorCode) { + synchronized (mLock) { + Pair<UtteranceId, RequestCallbacks> c = mCallbacks.remove(utteranceIdStr); + c.second.onSynthesisFailure(c.first, errorCode); + } + } + + /** + * Retrieve TTS engine status {@link EngineStatus}. Requires connected client. + */ + public EngineStatus getEngineStatus() { + synchronized (mLock) { + return mEngineStatus; + } + } + + /** + * Query TTS engine about available voices and defaults. + * + * @return EngineStatus is connected or null if client is disconnected. + */ + private EngineStatus requestEngineStatus(ITextToSpeechService service) + throws RemoteException { + List<VoiceInfo> voices = service.getVoicesInfo(); + if (voices == null) { + Log.e(TAG, "Requested engine doesn't support TTS V2 API"); + return null; + } + + return new EngineStatus(mServiceConnection.getEngineName(), voices); + } + + private class Connection implements ServiceConnection { + private final String mEngineName; + + private ITextToSpeechService mService; + + private boolean mEstablished; + + private PrepareConnectionAsyncTask mSetupConnectionAsyncTask; + + public Connection(String engineName) { + this.mEngineName = engineName; + } + + private final ITextToSpeechCallback.Stub mCallback = new ITextToSpeechCallback.Stub() { + + @Override + public void onStart(String utteranceIdStr) { + synchronized (mLock) { + Pair<UtteranceId, RequestCallbacks> callbacks = getCallback(utteranceIdStr); + callbacks.second.onSynthesisStart(callbacks.first); + } + } + + public void onStop(String utteranceIdStr) { + synchronized (mLock) { + Pair<UtteranceId, RequestCallbacks> callbacks = removeCallback(utteranceIdStr); + callbacks.second.onSynthesisStop(callbacks.first); + } + } + + @Override + public void onSuccess(String utteranceIdStr) { + synchronized (mLock) { + Pair<UtteranceId, RequestCallbacks> callbacks = removeCallback(utteranceIdStr); + callbacks.second.onSynthesisSuccess(callbacks.first); + } + } + + public void onFallback(String utteranceIdStr) { + synchronized (mLock) { + Pair<UtteranceId, RequestCallbacks> callbacks = getCallback( + utteranceIdStr); + callbacks.second.onSynthesisFallback(callbacks.first); + } + }; + + @Override + public void onError(String utteranceIdStr, int errorCode) { + removeCallbackAndErr(utteranceIdStr, errorCode); + } + + @Override + public void onVoicesInfoChange(List<VoiceInfo> voicesInfo) { + synchronized (mLock) { + mEngineStatus = new EngineStatus(mServiceConnection.getEngineName(), + voicesInfo); + mConnectionCallbacks.onEngineStatusChange(mEngineStatus); + } + } + }; + + private class PrepareConnectionAsyncTask extends AsyncTask<Void, Void, EngineStatus> { + + private final ComponentName mName; + + public PrepareConnectionAsyncTask(ComponentName name) { + mName = name; + } + + @Override + protected EngineStatus doInBackground(Void... params) { + synchronized(mLock) { + if (isCancelled()) { + return null; + } + try { + mService.setCallback(getCallerIdentity(), mCallback); + return requestEngineStatus(mService); + } catch (RemoteException re) { + Log.e(TAG, "Error setting up the TTS service"); + return null; + } + } + } + + @Override + protected void onPostExecute(EngineStatus result) { + synchronized(mLock) { + if (mSetupConnectionAsyncTask == this) { + mSetupConnectionAsyncTask = null; + } + if (result == null) { + Log.e(TAG, "Setup task failed"); + disconnect(); + mConnectionCallbacks.onConnectionFailure(); + return; + } + + mEngineStatus = result; + mEstablished = true; + } + mConnectionCallbacks.onConnectionSuccess(); + } + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + Log.i(TAG, "Connected to " + name); + + synchronized(mLock) { + mEstablished = false; + mService = ITextToSpeechService.Stub.asInterface(service); + startSetupConnectionTask(name); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + Log.i(TAG, "Asked to disconnect from " + name); + + synchronized(mLock) { + stopSetupConnectionTask(); + } + mConnectionCallbacks.onServiceDisconnected(); + } + + private void startSetupConnectionTask(ComponentName name) { + stopSetupConnectionTask(); + mSetupConnectionAsyncTask = new PrepareConnectionAsyncTask(name); + mSetupConnectionAsyncTask.execute(); + } + + private boolean stopSetupConnectionTask() { + boolean result = false; + if (mSetupConnectionAsyncTask != null) { + result = mSetupConnectionAsyncTask.cancel(false); + mSetupConnectionAsyncTask = null; + } + return result; + } + + IBinder getCallerIdentity() { + return mCallback; + } + + boolean isEstablished() { + return mService != null && mEstablished; + } + + boolean runAction(Action action) { + synchronized (mLock) { + try { + action.run(mService); + return true; + } catch (Exception ex) { + Log.e(TAG, action.getName() + " failed", ex); + disconnect(); + return false; + } + } + } + + void disconnect() { + mContext.unbindService(this); + stopSetupConnectionTask(); + mService = null; + mEstablished = false; + if (mServiceConnection == this) { + mServiceConnection = null; + } + } + + String getEngineName() { + return mEngineName; + } + } + + private abstract class Action { + private final String mName; + + public Action(String name) { + mName = name; + } + + public String getName() {return mName;} + abstract void run(ITextToSpeechService service) throws RemoteException; + } + + private IBinder getCallerIdentity() { + if (mServiceConnection != null) { + return mServiceConnection.getCallerIdentity(); + } + return null; + } + + private boolean runAction(Action action) { + synchronized (mLock) { + if (mServiceConnection == null) { + return false; + } + if (!mServiceConnection.isEstablished()) { + return false; + } + mServiceConnection.runAction(action); + return true; + } + } + + private static final String ACTION_STOP_NAME = "stop"; + + /** + * Interrupts the current utterance spoken (whether played or rendered to file) and discards + * other utterances in the queue. + */ + public void stop() { + runAction(new Action(ACTION_STOP_NAME) { + @Override + public void run(ITextToSpeechService service) throws RemoteException { + if (service.stop(getCallerIdentity()) != Status.SUCCESS) { + Log.e(TAG, "Stop failed"); + } + mCallbacks.clear(); + } + }); + } + + private static final String ACTION_QUEUE_SPEAK_NAME = "queueSpeak"; + + /** + * Speaks the string using the specified queuing strategy using current + * voice. This method is asynchronous, i.e. the method just adds the request + * to the queue of TTS requests and then returns. The synthesis might not + * have finished (or even started!) at the time when this method returns. + * + * @param utterance The string of text to be spoken. No longer than + * 1000 characters. + * @param utteranceId Unique identificator used to track the synthesis progress + * in {@link RequestCallbacks}. + * @param config Synthesis request configuration. Can't be null. Has to contain a + * voice. + * @param callbacks Synthesis request callbacks. If null, default request + * callbacks object will be used. + */ + public void queueSpeak(final String utterance, final UtteranceId utteranceId, + final RequestConfig config, + final RequestCallbacks callbacks) { + runAction(new Action(ACTION_QUEUE_SPEAK_NAME) { + @Override + public void run(ITextToSpeechService service) throws RemoteException { + RequestCallbacks c = mDefaultRequestCallbacks; + if (callbacks != null) { + c = callbacks; + } + int addCallbackStatus = addCallback(utteranceId, c); + if (addCallbackStatus != Status.SUCCESS) { + c.onSynthesisFailure(utteranceId, Status.ERROR_INVALID_REQUEST); + return; + } + + int queueResult = service.speakV2( + getCallerIdentity(), + new SynthesisRequestV2(utterance, utteranceId.toUniqueString(), config)); + if (queueResult != Status.SUCCESS) { + removeCallbackAndErr(utteranceId.toUniqueString(), queueResult); + } + } + }); + } + + private static final String ACTION_QUEUE_SYNTHESIZE_TO_FILE = "queueSynthesizeToFile"; + + /** + * Synthesizes the given text to a file using the specified parameters. This + * method is asynchronous, i.e. the method just adds the request to the + * queue of TTS requests and then returns. The synthesis might not have + * finished (or even started!) at the time when this method returns. + * + * @param utterance The text that should be synthesized. No longer than + * 1000 characters. + * @param utteranceId Unique identificator used to track the synthesis progress + * in {@link RequestCallbacks}. + * @param outputFile File to write the generated audio data to. + * @param config Synthesis request configuration. Can't be null. Have to contain a + * voice. + * @param callbacks Synthesis request callbacks. If null, default request + * callbacks object will be used. + */ + public void queueSynthesizeToFile(final String utterance, final UtteranceId utteranceId, + final File outputFile, final RequestConfig config, + final RequestCallbacks callbacks) { + runAction(new Action(ACTION_QUEUE_SYNTHESIZE_TO_FILE) { + @Override + public void run(ITextToSpeechService service) throws RemoteException { + RequestCallbacks c = mDefaultRequestCallbacks; + if (callbacks != null) { + c = callbacks; + } + int addCallbackStatus = addCallback(utteranceId, c); + if (addCallbackStatus != Status.SUCCESS) { + c.onSynthesisFailure(utteranceId, Status.ERROR_INVALID_REQUEST); + return; + } + + ParcelFileDescriptor fileDescriptor = null; + try { + if (outputFile.exists() && !outputFile.canWrite()) { + Log.e(TAG, "No permissions to write to " + outputFile); + removeCallbackAndErr(utteranceId.toUniqueString(), Status.ERROR_OUTPUT); + return; + } + fileDescriptor = ParcelFileDescriptor.open(outputFile, + ParcelFileDescriptor.MODE_WRITE_ONLY | + ParcelFileDescriptor.MODE_CREATE | + ParcelFileDescriptor.MODE_TRUNCATE); + + int queueResult = service.synthesizeToFileDescriptorV2(getCallerIdentity(), + fileDescriptor, + new SynthesisRequestV2(utterance, utteranceId.toUniqueString(), + config)); + fileDescriptor.close(); + if (queueResult != Status.SUCCESS) { + removeCallbackAndErr(utteranceId.toUniqueString(), queueResult); + } + } catch (FileNotFoundException e) { + Log.e(TAG, "Opening file " + outputFile + " failed", e); + removeCallbackAndErr(utteranceId.toUniqueString(), Status.ERROR_OUTPUT); + } catch (IOException e) { + Log.e(TAG, "Closing file " + outputFile + " failed", e); + removeCallbackAndErr(utteranceId.toUniqueString(), Status.ERROR_OUTPUT); + } + } + }); + } + + private static final String ACTION_QUEUE_SILENCE_NAME = "queueSilence"; + + /** + * Plays silence for the specified amount of time. This method is asynchronous, + * i.e. the method just adds the request to the queue of TTS requests and then + * returns. The synthesis might not have finished (or even started!) at the time + * when this method returns. + * + * @param durationInMs The duration of the silence in milliseconds. + * @param utteranceId Unique identificator used to track the synthesis progress + * in {@link RequestCallbacks}. + * @param callbacks Synthesis request callbacks. If null, default request + * callbacks object will be used. + */ + public void queueSilence(final long durationInMs, final UtteranceId utteranceId, + final RequestCallbacks callbacks) { + runAction(new Action(ACTION_QUEUE_SILENCE_NAME) { + @Override + public void run(ITextToSpeechService service) throws RemoteException { + RequestCallbacks c = mDefaultRequestCallbacks; + if (callbacks != null) { + c = callbacks; + } + int addCallbackStatus = addCallback(utteranceId, c); + if (addCallbackStatus != Status.SUCCESS) { + c.onSynthesisFailure(utteranceId, Status.ERROR_INVALID_REQUEST); + } + + int queueResult = service.playSilence(getCallerIdentity(), durationInMs, + TextToSpeech.QUEUE_ADD, utteranceId.toUniqueString()); + + if (queueResult != Status.SUCCESS) { + removeCallbackAndErr(utteranceId.toUniqueString(), queueResult); + } + } + }); + } + + + private static final String ACTION_QUEUE_AUDIO_NAME = "queueAudio"; + + /** + * Plays the audio resource using the specified parameters. + * This method is asynchronous, i.e. the method just adds the request to the queue of TTS + * requests and then returns. The synthesis might not have finished (or even started!) at the + * time when this method returns. + * + * @param audioUrl The audio resource that should be played + * @param utteranceId Unique identificator used to track synthesis progress + * in {@link RequestCallbacks}. + * @param config Synthesis request configuration. Can't be null. Doesn't have to contain a + * voice (only system parameters are used). + * @param callbacks Synthesis request callbacks. If null, default request + * callbacks object will be used. + */ + public void queueAudio(final Uri audioUrl, final UtteranceId utteranceId, + final RequestConfig config, final RequestCallbacks callbacks) { + runAction(new Action(ACTION_QUEUE_AUDIO_NAME) { + @Override + public void run(ITextToSpeechService service) throws RemoteException { + RequestCallbacks c = mDefaultRequestCallbacks; + if (callbacks != null) { + c = callbacks; + } + int addCallbackStatus = addCallback(utteranceId, c); + if (addCallbackStatus != Status.SUCCESS) { + c.onSynthesisFailure(utteranceId, Status.ERROR_INVALID_REQUEST); + } + + int queueResult = service.playAudioV2(getCallerIdentity(), audioUrl, + utteranceId.toUniqueString(), config.getVoiceParams()); + + if (queueResult != Status.SUCCESS) { + removeCallbackAndErr(utteranceId.toUniqueString(), queueResult); + } + } + }); + } +} diff --git a/core/java/android/speech/tts/TextToSpeechService.java b/core/java/android/speech/tts/TextToSpeechService.java index 575855c..d7c51fc 100644 --- a/core/java/android/speech/tts/TextToSpeechService.java +++ b/core/java/android/speech/tts/TextToSpeechService.java @@ -34,26 +34,27 @@ import android.speech.tts.TextToSpeech.Engine; import android.text.TextUtils; import android.util.Log; -import java.io.FileDescriptor; import java.io.FileOutputStream; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Locale; +import java.util.Map; +import java.util.MissingResourceException; import java.util.Set; /** * Abstract base class for TTS engine implementations. The following methods - * need to be implemented. - * + * need to be implemented for V1 API ({@link TextToSpeech}) implementation. * <ul> - * <li>{@link #onIsLanguageAvailable}</li> - * <li>{@link #onLoadLanguage}</li> - * <li>{@link #onGetLanguage}</li> - * <li>{@link #onSynthesizeText}</li> - * <li>{@link #onStop}</li> + * <li>{@link #onIsLanguageAvailable}</li> + * <li>{@link #onLoadLanguage}</li> + * <li>{@link #onGetLanguage}</li> + * <li>{@link #onSynthesizeText}</li> + * <li>{@link #onStop}</li> * </ul> - * * The first three deal primarily with language management, and are used to * query the engine for it's support for a given language and indicate to it * that requests in a given language are imminent. @@ -61,22 +62,44 @@ import java.util.Set; * {@link #onSynthesizeText} is central to the engine implementation. The * implementation should synthesize text as per the request parameters and * return synthesized data via the supplied callback. This class and its helpers - * will then consume that data, which might mean queueing it for playback or writing - * it to a file or similar. All calls to this method will be on a single - * thread, which will be different from the main thread of the service. Synthesis - * must be synchronous which means the engine must NOT hold on the callback or call - * any methods on it after the method returns + * will then consume that data, which might mean queuing it for playback or writing + * it to a file or similar. All calls to this method will be on a single thread, + * which will be different from the main thread of the service. Synthesis must be + * synchronous which means the engine must NOT hold on to the callback or call any + * methods on it after the method returns. * - * {@link #onStop} tells the engine that it should stop all ongoing synthesis, if - * any. Any pending data from the current synthesis will be discarded. + * {@link #onStop} tells the engine that it should stop + * all ongoing synthesis, if any. Any pending data from the current synthesis + * will be discarded. * + * {@link #onGetLanguage} is not required as of JELLYBEAN_MR2 (API 18) and later, it is only + * called on earlier versions of Android. + * <p> + * In order to fully support the V2 API ({@link TextToSpeechClient}), + * these methods must be implemented: + * <ul> + * <li>{@link #onSynthesizeTextV2}</li> + * <li>{@link #checkVoicesInfo}</li> + * <li>{@link #onVoicesInfoChange}</li> + * <li>{@link #implementsV2API}</li> + * </ul> + * In addition {@link #implementsV2API} has to return true. + * <p> + * If the service does not implement these methods and {@link #implementsV2API} returns false, + * then the V2 API will be provided by converting V2 requests ({@link #onSynthesizeTextV2}) + * to V1 requests ({@link #onSynthesizeText}). On service setup, all of the available device + * locales will be fed to {@link #onIsLanguageAvailable} to check if they are supported. + * If they are, embedded and/or network voices will be created depending on the result of + * {@link #onGetFeaturesForLanguage}. + * <p> + * Note that a V2 service will still receive requests from V1 clients and has to implement all + * of the V1 API methods. */ public abstract class TextToSpeechService extends Service { private static final boolean DBG = false; private static final String TAG = "TextToSpeechService"; - private static final String SYNTH_THREAD_NAME = "SynthThread"; private SynthHandler mSynthHandler; @@ -89,6 +112,11 @@ public abstract class TextToSpeechService extends Service { private CallbackMap mCallbacks; private String mPackageName; + private final Object mVoicesInfoLock = new Object(); + + private List<VoiceInfo> mVoicesInfoList; + private Map<String, VoiceInfo> mVoicesInfoLookup; + @Override public void onCreate() { if (DBG) Log.d(TAG, "onCreate()"); @@ -108,6 +136,7 @@ public abstract class TextToSpeechService extends Service { mPackageName = getApplicationInfo().packageName; String[] defaultLocale = getSettingsLocale(); + // Load default language onLoadLanguage(defaultLocale[0], defaultLocale[1], defaultLocale[2]); } @@ -148,6 +177,9 @@ public abstract class TextToSpeechService extends Service { /** * Returns the language, country and variant currently being used by the TTS engine. * + * This method will be called only on Android 4.2 and before (API <= 17). In later versions + * this method is not called by the Android TTS framework. + * * Can be called on multiple threads. * * @return A 3-element array, containing language (ISO 3-letter code), @@ -191,21 +223,159 @@ public abstract class TextToSpeechService extends Service { protected abstract void onStop(); /** - * Tells the service to synthesize speech from the given text. This method should - * block until the synthesis is finished. - * - * Called on the synthesis thread. + * Tells the service to synthesize speech from the given text. This method + * should block until the synthesis is finished. Used for requests from V1 + * clients ({@link android.speech.tts.TextToSpeech}). Called on the synthesis + * thread. * * @param request The synthesis request. - * @param callback The callback the the engine must use to make data available for - * playback or for writing to a file. + * @param callback The callback that the engine must use to make data + * available for playback or for writing to a file. */ protected abstract void onSynthesizeText(SynthesisRequest request, SynthesisCallback callback); /** + * Check the available voices data and return an immutable list of the available voices. + * The output of this method will be passed to clients to allow them to configure synthesis + * requests. + * + * Can be called on multiple threads. + * + * The result of this method will be saved and served to all TTS clients. If a TTS service wants + * to update the set of available voices, it should call the {@link #forceVoicesInfoCheck()} + * method. + */ + protected List<VoiceInfo> checkVoicesInfo() { + if (implementsV2API()) { + throw new IllegalStateException("For proper V2 API implementation this method has to" + + " be implemented"); + } + + // V2 to V1 interface adapter. This allows using V2 client interface on V1-only services. + Bundle defaultParams = new Bundle(); + defaultParams.putFloat(TextToSpeechClient.Params.SPEECH_PITCH, 1.0f); + defaultParams.putFloat(TextToSpeechClient.Params.SPEECH_SPEED, -1.0f); + + // Enumerate all locales and check if they are available + ArrayList<VoiceInfo> voicesInfo = new ArrayList<VoiceInfo>(); + int id = 0; + for (Locale locale : Locale.getAvailableLocales()) { + int expectedStatus = TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE; + if (locale.getVariant().isEmpty()) { + if (locale.getCountry().isEmpty()) { + expectedStatus = TextToSpeech.LANG_AVAILABLE; + } else { + expectedStatus = TextToSpeech.LANG_COUNTRY_AVAILABLE; + } + } + try { + int localeStatus = onIsLanguageAvailable(locale.getISO3Language(), + locale.getISO3Country(), locale.getVariant()); + if (localeStatus != expectedStatus) { + continue; + } + } catch (MissingResourceException e) { + // Ignore locale without iso 3 codes + continue; + } + + Set<String> features = onGetFeaturesForLanguage(locale.getISO3Language(), + locale.getISO3Country(), locale.getVariant()); + + VoiceInfo.Builder builder = new VoiceInfo.Builder(); + builder.setLatency(VoiceInfo.LATENCY_NORMAL); + builder.setQuality(VoiceInfo.QUALITY_NORMAL); + builder.setLocale(locale); + builder.setParamsWithDefaults(defaultParams); + + if (features == null || features.contains( + TextToSpeech.Engine.KEY_FEATURE_EMBEDDED_SYNTHESIS)) { + builder.setName(locale.toString() + "-embedded"); + builder.setRequiresNetworkConnection(false); + voicesInfo.add(builder.build()); + } + + if (features != null && features.contains( + TextToSpeech.Engine.KEY_FEATURE_NETWORK_SYNTHESIS)) { + builder.setName(locale.toString() + "-network"); + builder.setRequiresNetworkConnection(true); + voicesInfo.add(builder.build()); + } + } + + return voicesInfo; + } + + /** + * Tells the synthesis thread that it should reload voice data. + * There's a high probability that the underlying set of available voice data has changed. + * Called only on the synthesis thread. + */ + protected void onVoicesInfoChange() { + + } + + /** + * Tells the service to synthesize speech from the given text. This method + * should block until the synthesis is finished. Used for requests from V2 + * client {@link android.speech.tts.TextToSpeechClient}. Called on the + * synthesis thread. + * + * @param request The synthesis request. + * @param callback The callback the the engine must use to make data + * available for playback or for writing to a file. + */ + protected void onSynthesizeTextV2(SynthesisRequestV2 request, + VoiceInfo selectedVoice, + SynthesisCallback callback) { + if (implementsV2API()) { + throw new IllegalStateException("For proper V2 API implementation this method has to" + + " be implemented"); + } + + // Convert to V1 params + int speechRate = (int) (request.getVoiceParams().getFloat( + TextToSpeechClient.Params.SPEECH_SPEED, 1.0f) * 100); + int speechPitch = (int) (request.getVoiceParams().getFloat( + TextToSpeechClient.Params.SPEECH_PITCH, 1.0f) * 100); + + // Provide adapter to V1 API + Bundle params = new Bundle(); + params.putString(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, request.getUtteranceId()); + params.putInt(TextToSpeech.Engine.KEY_PARAM_PITCH, speechPitch); + params.putInt(TextToSpeech.Engine.KEY_PARAM_RATE, speechRate); + if (selectedVoice.getRequiresNetworkConnection()) { + params.putString(TextToSpeech.Engine.KEY_FEATURE_NETWORK_SYNTHESIS, "true"); + } else { + params.putString(TextToSpeech.Engine.KEY_FEATURE_EMBEDDED_SYNTHESIS, "true"); + } + + // Build V1 request + SynthesisRequest requestV1 = new SynthesisRequest(request.getText(), params); + Locale locale = selectedVoice.getLocale(); + requestV1.setLanguage(locale.getISO3Language(), locale.getISO3Country(), + locale.getVariant()); + requestV1.setSpeechRate(speechRate); + requestV1.setPitch(speechPitch); + + // Synthesize using V1 interface + onSynthesizeText(requestV1, callback); + } + + /** + * If true, this service implements proper V2 TTS API service. If it's false, + * V2 API will be provided through adapter. + */ + protected boolean implementsV2API() { + return false; + } + + /** * Queries the service for a set of features supported for a given language. * + * Can be called on multiple threads. + * * @param lang ISO-3 language code. * @param country ISO-3 country code. May be empty or null. * @param variant Language variant. May be empty or null. @@ -215,6 +385,69 @@ public abstract class TextToSpeechService extends Service { return null; } + private List<VoiceInfo> getVoicesInfo() { + synchronized (mVoicesInfoLock) { + if (mVoicesInfoList == null) { + // Get voices. Defensive copy to make sure TTS engine won't alter the list. + mVoicesInfoList = new ArrayList<VoiceInfo>(checkVoicesInfo()); + // Build lookup map + mVoicesInfoLookup = new HashMap<String, VoiceInfo>((int) ( + mVoicesInfoList.size()*1.5f)); + for (VoiceInfo voiceInfo : mVoicesInfoList) { + VoiceInfo prev = mVoicesInfoLookup.put(voiceInfo.getName(), voiceInfo); + if (prev != null) { + Log.e(TAG, "Duplicate name (" + voiceInfo.getName() + ") of the voice "); + } + } + } + return mVoicesInfoList; + } + } + + public VoiceInfo getVoicesInfoWithName(String name) { + synchronized (mVoicesInfoLock) { + if (mVoicesInfoLookup != null) { + return mVoicesInfoLookup.get(name); + } + } + return null; + } + + /** + * Force TTS service to reevaluate the set of available languages. Will result in + * a call to {@link #checkVoicesInfo()} on the same thread, {@link #onVoicesInfoChange} + * on the synthesizer thread and callback to + * {@link TextToSpeechClient.ConnectionCallbacks#onEngineStatusChange} of all connected + * TTS clients. + * + * Use this method only if you know that set of available languages changed. + * + * Can be called on multiple threads. + */ + public void forceVoicesInfoCheck() { + synchronized (mVoicesInfoLock) { + List<VoiceInfo> old = mVoicesInfoList; + + mVoicesInfoList = null; // Force recreation of voices info list + getVoicesInfo(); + + if (mVoicesInfoList == null) { + throw new IllegalStateException("This method applies only to services " + + "supporting V2 TTS API. This services doesn't support V2 TTS API."); + } + + if (old != null) { + // Flush all existing items, and inform synthesis thread about the change. + mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_FLUSH, + new VoicesInfoChangeItem()); + // TODO: Handle items that may be added to queue after SynthesizerRestartItem + // but before client reconnection + // Disconnect all of them + mCallbacks.dispatchVoicesInfoChange(mVoicesInfoList); + } + } + } + private int getDefaultSpeechRate() { return getSecureSettingInt(Settings.Secure.TTS_DEFAULT_RATE, Engine.DEFAULT_RATE); } @@ -317,7 +550,8 @@ public abstract class TextToSpeechService extends Service { if (!speechItem.isValid()) { if (utterenceProgress != null) { - utterenceProgress.dispatchOnError(); + utterenceProgress.dispatchOnError( + TextToSpeechClient.Status.ERROR_INVALID_REQUEST); } return TextToSpeech.ERROR; } @@ -342,12 +576,13 @@ public abstract class TextToSpeechService extends Service { // // Note that this string is interned, so the == comparison works. msg.obj = speechItem.getCallerIdentity(); + if (sendMessage(msg)) { return TextToSpeech.SUCCESS; } else { Log.w(TAG, "SynthThread has quit"); if (utterenceProgress != null) { - utterenceProgress.dispatchOnError(); + utterenceProgress.dispatchOnError(TextToSpeechClient.Status.ERROR_SERVICE); } return TextToSpeech.ERROR; } @@ -399,9 +634,11 @@ public abstract class TextToSpeechService extends Service { } interface UtteranceProgressDispatcher { - public void dispatchOnDone(); + public void dispatchOnFallback(); + public void dispatchOnStop(); + public void dispatchOnSuccess(); public void dispatchOnStart(); - public void dispatchOnError(); + public void dispatchOnError(int errorCode); } /** @@ -409,15 +646,13 @@ public abstract class TextToSpeechService extends Service { */ private abstract class SpeechItem { private final Object mCallerIdentity; - protected final Bundle mParams; private final int mCallerUid; private final int mCallerPid; private boolean mStarted = false; private boolean mStopped = false; - public SpeechItem(Object caller, int callerUid, int callerPid, Bundle params) { + public SpeechItem(Object caller, int callerUid, int callerPid) { mCallerIdentity = caller; - mParams = params; mCallerUid = callerUid; mCallerPid = callerPid; } @@ -446,20 +681,18 @@ public abstract class TextToSpeechService extends Service { * Must not be called more than once. * * Only called on the synthesis thread. - * - * @return {@link TextToSpeech#SUCCESS} or {@link TextToSpeech#ERROR}. */ - public int play() { + public void play() { synchronized (this) { if (mStarted) { throw new IllegalStateException("play() called twice"); } mStarted = true; } - return playImpl(); + playImpl(); } - protected abstract int playImpl(); + protected abstract void playImpl(); /** * Stops the speech item. @@ -485,20 +718,37 @@ public abstract class TextToSpeechService extends Service { } /** - * An item in the synth thread queue that process utterance. + * An item in the synth thread queue that process utterance (and call back to client about + * progress). */ private abstract class UtteranceSpeechItem extends SpeechItem implements UtteranceProgressDispatcher { - public UtteranceSpeechItem(Object caller, int callerUid, int callerPid, Bundle params) { - super(caller, callerUid, callerPid, params); + public UtteranceSpeechItem(Object caller, int callerUid, int callerPid) { + super(caller, callerUid, callerPid); + } + + @Override + public void dispatchOnSuccess() { + final String utteranceId = getUtteranceId(); + if (utteranceId != null) { + mCallbacks.dispatchOnSuccess(getCallerIdentity(), utteranceId); + } } @Override - public void dispatchOnDone() { + public void dispatchOnStop() { final String utteranceId = getUtteranceId(); if (utteranceId != null) { - mCallbacks.dispatchOnDone(getCallerIdentity(), utteranceId); + mCallbacks.dispatchOnStop(getCallerIdentity(), utteranceId); + } + } + + @Override + public void dispatchOnFallback() { + final String utteranceId = getUtteranceId(); + if (utteranceId != null) { + mCallbacks.dispatchOnFallback(getCallerIdentity(), utteranceId); } } @@ -511,44 +761,260 @@ public abstract class TextToSpeechService extends Service { } @Override - public void dispatchOnError() { + public void dispatchOnError(int errorCode) { final String utteranceId = getUtteranceId(); if (utteranceId != null) { - mCallbacks.dispatchOnError(getCallerIdentity(), utteranceId); + mCallbacks.dispatchOnError(getCallerIdentity(), utteranceId, errorCode); } } - public int getStreamType() { - return getIntParam(Engine.KEY_PARAM_STREAM, Engine.DEFAULT_STREAM); + abstract public String getUtteranceId(); + + String getStringParam(Bundle params, String key, String defaultValue) { + return params == null ? defaultValue : params.getString(key, defaultValue); + } + + int getIntParam(Bundle params, String key, int defaultValue) { + return params == null ? defaultValue : params.getInt(key, defaultValue); + } + + float getFloatParam(Bundle params, String key, float defaultValue) { + return params == null ? defaultValue : params.getFloat(key, defaultValue); + } + } + + /** + * UtteranceSpeechItem for V1 API speech items. V1 API speech items keep + * synthesis parameters in a single Bundle passed as parameter. This class + * allow subclasses to access them conveniently. + */ + private abstract class SpeechItemV1 extends UtteranceSpeechItem { + protected final Bundle mParams; + + SpeechItemV1(Object callerIdentity, int callerUid, int callerPid, + Bundle params) { + super(callerIdentity, callerUid, callerPid); + mParams = params; + } + + boolean hasLanguage() { + return !TextUtils.isEmpty(getStringParam(mParams, Engine.KEY_PARAM_LANGUAGE, null)); } - public float getVolume() { - return getFloatParam(Engine.KEY_PARAM_VOLUME, Engine.DEFAULT_VOLUME); + int getSpeechRate() { + return getIntParam(mParams, Engine.KEY_PARAM_RATE, getDefaultSpeechRate()); } - public float getPan() { - return getFloatParam(Engine.KEY_PARAM_PAN, Engine.DEFAULT_PAN); + int getPitch() { + return getIntParam(mParams, Engine.KEY_PARAM_PITCH, Engine.DEFAULT_PITCH); } + @Override public String getUtteranceId() { - return getStringParam(Engine.KEY_PARAM_UTTERANCE_ID, null); + return getStringParam(mParams, Engine.KEY_PARAM_UTTERANCE_ID, null); + } + + int getStreamType() { + return getIntParam(mParams, Engine.KEY_PARAM_STREAM, Engine.DEFAULT_STREAM); + } + + float getVolume() { + return getFloatParam(mParams, Engine.KEY_PARAM_VOLUME, Engine.DEFAULT_VOLUME); + } + + float getPan() { + return getFloatParam(mParams, Engine.KEY_PARAM_PAN, Engine.DEFAULT_PAN); + } + } + + class SynthesisSpeechItemV2 extends UtteranceSpeechItem { + private final SynthesisRequestV2 mSynthesisRequest; + private AbstractSynthesisCallback mSynthesisCallback; + private final EventLoggerV2 mEventLogger; + + public SynthesisSpeechItemV2(Object callerIdentity, int callerUid, int callerPid, + SynthesisRequestV2 synthesisRequest) { + super(callerIdentity, callerUid, callerPid); + + mSynthesisRequest = synthesisRequest; + mEventLogger = new EventLoggerV2(synthesisRequest, callerUid, callerPid, + mPackageName); + + updateSpeechSpeedParam(synthesisRequest); + } + + private void updateSpeechSpeedParam(SynthesisRequestV2 synthesisRequest) { + Bundle voiceParams = mSynthesisRequest.getVoiceParams(); + + // Inject default speech speed if needed + if (voiceParams.containsKey(TextToSpeechClient.Params.SPEECH_SPEED)) { + if (voiceParams.getFloat(TextToSpeechClient.Params.SPEECH_SPEED) <= 0) { + voiceParams.putFloat(TextToSpeechClient.Params.SPEECH_SPEED, + getDefaultSpeechRate() / 100.0f); + } + } } - protected String getStringParam(String key, String defaultValue) { - return mParams == null ? defaultValue : mParams.getString(key, defaultValue); + @Override + public boolean isValid() { + if (mSynthesisRequest.getText() == null) { + Log.e(TAG, "null synthesis text"); + return false; + } + if (mSynthesisRequest.getText().length() >= TextToSpeech.getMaxSpeechInputLength()) { + Log.w(TAG, "Text too long: " + mSynthesisRequest.getText().length() + " chars"); + return false; + } + + return true; } - protected int getIntParam(String key, int defaultValue) { - return mParams == null ? defaultValue : mParams.getInt(key, defaultValue); + @Override + protected void playImpl() { + AbstractSynthesisCallback synthesisCallback; + if (mEventLogger != null) { + mEventLogger.onRequestProcessingStart(); + } + synchronized (this) { + // stop() might have been called before we enter this + // synchronized block. + if (isStopped()) { + return; + } + mSynthesisCallback = createSynthesisCallback(); + synthesisCallback = mSynthesisCallback; + } + + // Get voice info + VoiceInfo voiceInfo = getVoicesInfoWithName(mSynthesisRequest.getVoiceName()); + if (voiceInfo != null) { + // Primary voice + TextToSpeechService.this.onSynthesizeTextV2(mSynthesisRequest, voiceInfo, + synthesisCallback); + } else { + Log.e(TAG, "Unknown voice name:" + mSynthesisRequest.getVoiceName()); + synthesisCallback.error(TextToSpeechClient.Status.ERROR_INVALID_REQUEST); + } + + // Fix for case where client called .start() & .error(), but did not called .done() + if (!synthesisCallback.hasFinished()) { + synthesisCallback.done(); + } } - protected float getFloatParam(String key, float defaultValue) { - return mParams == null ? defaultValue : mParams.getFloat(key, defaultValue); + @Override + protected void stopImpl() { + AbstractSynthesisCallback synthesisCallback; + synchronized (this) { + synthesisCallback = mSynthesisCallback; + } + if (synthesisCallback != null) { + // If the synthesis callback is null, it implies that we haven't + // entered the synchronized(this) block in playImpl which in + // turn implies that synthesis would not have started. + synthesisCallback.stop(); + TextToSpeechService.this.onStop(); + } } + protected AbstractSynthesisCallback createSynthesisCallback() { + return new PlaybackSynthesisCallback(getStreamType(), getVolume(), getPan(), + mAudioPlaybackHandler, this, getCallerIdentity(), mEventLogger, + implementsV2API()); + } + + private int getStreamType() { + return getIntParam(mSynthesisRequest.getAudioParams(), + TextToSpeechClient.Params.AUDIO_PARAM_STREAM, + Engine.DEFAULT_STREAM); + } + + private float getVolume() { + return getFloatParam(mSynthesisRequest.getAudioParams(), + TextToSpeechClient.Params.AUDIO_PARAM_VOLUME, + Engine.DEFAULT_VOLUME); + } + + private float getPan() { + return getFloatParam(mSynthesisRequest.getAudioParams(), + TextToSpeechClient.Params.AUDIO_PARAM_PAN, + Engine.DEFAULT_PAN); + } + + @Override + public String getUtteranceId() { + return mSynthesisRequest.getUtteranceId(); + } + } + + private class SynthesisToFileOutputStreamSpeechItemV2 extends SynthesisSpeechItemV2 { + private final FileOutputStream mFileOutputStream; + + public SynthesisToFileOutputStreamSpeechItemV2(Object callerIdentity, int callerUid, + int callerPid, + SynthesisRequestV2 synthesisRequest, + FileOutputStream fileOutputStream) { + super(callerIdentity, callerUid, callerPid, synthesisRequest); + mFileOutputStream = fileOutputStream; + } + + @Override + protected AbstractSynthesisCallback createSynthesisCallback() { + return new FileSynthesisCallback(mFileOutputStream.getChannel(), + this, getCallerIdentity(), implementsV2API()); + } + + @Override + protected void playImpl() { + super.playImpl(); + try { + mFileOutputStream.close(); + } catch(IOException e) { + Log.w(TAG, "Failed to close output file", e); + } + } + } + + private class AudioSpeechItemV2 extends UtteranceSpeechItem { + private final AudioPlaybackQueueItem mItem; + private final Bundle mAudioParams; + private final String mUtteranceId; + + public AudioSpeechItemV2(Object callerIdentity, int callerUid, int callerPid, + String utteranceId, Bundle audioParams, Uri uri) { + super(callerIdentity, callerUid, callerPid); + mUtteranceId = utteranceId; + mAudioParams = audioParams; + mItem = new AudioPlaybackQueueItem(this, getCallerIdentity(), + TextToSpeechService.this, uri, getStreamType()); + } + + @Override + public boolean isValid() { + return true; + } + + @Override + protected void playImpl() { + mAudioPlaybackHandler.enqueue(mItem); + } + + @Override + protected void stopImpl() { + // Do nothing. + } + + protected int getStreamType() { + return mAudioParams.getInt(TextToSpeechClient.Params.AUDIO_PARAM_STREAM); + } + + public String getUtteranceId() { + return mUtteranceId; + } } - class SynthesisSpeechItem extends UtteranceSpeechItem { + + class SynthesisSpeechItemV1 extends SpeechItemV1 { // Never null. private final String mText; private final SynthesisRequest mSynthesisRequest; @@ -556,10 +1022,10 @@ public abstract class TextToSpeechService extends Service { // Non null after synthesis has started, and all accesses // guarded by 'this'. private AbstractSynthesisCallback mSynthesisCallback; - private final EventLogger mEventLogger; + private final EventLoggerV1 mEventLogger; private final int mCallerUid; - public SynthesisSpeechItem(Object callerIdentity, int callerUid, int callerPid, + public SynthesisSpeechItemV1(Object callerIdentity, int callerUid, int callerPid, Bundle params, String text) { super(callerIdentity, callerUid, callerPid, params); mText = text; @@ -567,7 +1033,7 @@ public abstract class TextToSpeechService extends Service { mSynthesisRequest = new SynthesisRequest(mText, mParams); mDefaultLocale = getSettingsLocale(); setRequestParams(mSynthesisRequest); - mEventLogger = new EventLogger(mSynthesisRequest, callerUid, callerPid, + mEventLogger = new EventLoggerV1(mSynthesisRequest, callerUid, callerPid, mPackageName); } @@ -589,25 +1055,30 @@ public abstract class TextToSpeechService extends Service { } @Override - protected int playImpl() { + protected void playImpl() { AbstractSynthesisCallback synthesisCallback; mEventLogger.onRequestProcessingStart(); synchronized (this) { // stop() might have been called before we enter this // synchronized block. if (isStopped()) { - return TextToSpeech.ERROR; + return; } mSynthesisCallback = createSynthesisCallback(); synthesisCallback = mSynthesisCallback; } + TextToSpeechService.this.onSynthesizeText(mSynthesisRequest, synthesisCallback); - return synthesisCallback.isDone() ? TextToSpeech.SUCCESS : TextToSpeech.ERROR; + + // Fix for case where client called .start() & .error(), but did not called .done() + if (synthesisCallback.hasStarted() && !synthesisCallback.hasFinished()) { + synthesisCallback.done(); + } } protected AbstractSynthesisCallback createSynthesisCallback() { return new PlaybackSynthesisCallback(getStreamType(), getVolume(), getPan(), - mAudioPlaybackHandler, this, getCallerIdentity(), mEventLogger); + mAudioPlaybackHandler, this, getCallerIdentity(), mEventLogger, false); } private void setRequestParams(SynthesisRequest request) { @@ -632,37 +1103,25 @@ public abstract class TextToSpeechService extends Service { } } - public String getLanguage() { - return getStringParam(Engine.KEY_PARAM_LANGUAGE, mDefaultLocale[0]); - } - - private boolean hasLanguage() { - return !TextUtils.isEmpty(getStringParam(Engine.KEY_PARAM_LANGUAGE, null)); - } - private String getCountry() { if (!hasLanguage()) return mDefaultLocale[1]; - return getStringParam(Engine.KEY_PARAM_COUNTRY, ""); + return getStringParam(mParams, Engine.KEY_PARAM_COUNTRY, ""); } private String getVariant() { if (!hasLanguage()) return mDefaultLocale[2]; - return getStringParam(Engine.KEY_PARAM_VARIANT, ""); + return getStringParam(mParams, Engine.KEY_PARAM_VARIANT, ""); } - private int getSpeechRate() { - return getIntParam(Engine.KEY_PARAM_RATE, getDefaultSpeechRate()); - } - - private int getPitch() { - return getIntParam(Engine.KEY_PARAM_PITCH, Engine.DEFAULT_PITCH); + public String getLanguage() { + return getStringParam(mParams, Engine.KEY_PARAM_LANGUAGE, mDefaultLocale[0]); } } - private class SynthesisToFileOutputStreamSpeechItem extends SynthesisSpeechItem { + private class SynthesisToFileOutputStreamSpeechItemV1 extends SynthesisSpeechItemV1 { private final FileOutputStream mFileOutputStream; - public SynthesisToFileOutputStreamSpeechItem(Object callerIdentity, int callerUid, + public SynthesisToFileOutputStreamSpeechItemV1(Object callerIdentity, int callerUid, int callerPid, Bundle params, String text, FileOutputStream fileOutputStream) { super(callerIdentity, callerUid, callerPid, params, text); mFileOutputStream = fileOutputStream; @@ -670,30 +1129,26 @@ public abstract class TextToSpeechService extends Service { @Override protected AbstractSynthesisCallback createSynthesisCallback() { - return new FileSynthesisCallback(mFileOutputStream.getChannel()); + return new FileSynthesisCallback(mFileOutputStream.getChannel(), + this, getCallerIdentity(), false); } @Override - protected int playImpl() { + protected void playImpl() { dispatchOnStart(); - int status = super.playImpl(); - if (status == TextToSpeech.SUCCESS) { - dispatchOnDone(); - } else { - dispatchOnError(); - } + super.playImpl(); try { mFileOutputStream.close(); } catch(IOException e) { Log.w(TAG, "Failed to close output file", e); } - return status; } } - private class AudioSpeechItem extends UtteranceSpeechItem { + private class AudioSpeechItemV1 extends SpeechItemV1 { private final AudioPlaybackQueueItem mItem; - public AudioSpeechItem(Object callerIdentity, int callerUid, int callerPid, + + public AudioSpeechItemV1(Object callerIdentity, int callerUid, int callerPid, Bundle params, Uri uri) { super(callerIdentity, callerUid, callerPid, params); mItem = new AudioPlaybackQueueItem(this, getCallerIdentity(), @@ -706,23 +1161,29 @@ public abstract class TextToSpeechService extends Service { } @Override - protected int playImpl() { + protected void playImpl() { mAudioPlaybackHandler.enqueue(mItem); - return TextToSpeech.SUCCESS; } @Override protected void stopImpl() { // Do nothing. } + + @Override + public String getUtteranceId() { + return getStringParam(mParams, Engine.KEY_PARAM_UTTERANCE_ID, null); + } } private class SilenceSpeechItem extends UtteranceSpeechItem { private final long mDuration; + private final String mUtteranceId; public SilenceSpeechItem(Object callerIdentity, int callerUid, int callerPid, - Bundle params, long duration) { - super(callerIdentity, callerUid, callerPid, params); + String utteranceId, long duration) { + super(callerIdentity, callerUid, callerPid); + mUtteranceId = utteranceId; mDuration = duration; } @@ -732,26 +1193,57 @@ public abstract class TextToSpeechService extends Service { } @Override - protected int playImpl() { + protected void playImpl() { mAudioPlaybackHandler.enqueue(new SilencePlaybackQueueItem( this, getCallerIdentity(), mDuration)); - return TextToSpeech.SUCCESS; } @Override protected void stopImpl() { - // Do nothing, handled by AudioPlaybackHandler#stopForApp + + } + + @Override + public String getUtteranceId() { + return mUtteranceId; + } + } + + /** + * Call {@link TextToSpeechService#onVoicesInfoChange} on synthesis thread. + */ + private class VoicesInfoChangeItem extends SpeechItem { + public VoicesInfoChangeItem() { + super(null, 0, 0); // It's never initiated by an user + } + + @Override + public boolean isValid() { + return true; + } + + @Override + protected void playImpl() { + TextToSpeechService.this.onVoicesInfoChange(); + } + + @Override + protected void stopImpl() { + // No-op } } + /** + * Call {@link TextToSpeechService#onLoadLanguage} on synth thread. + */ private class LoadLanguageItem extends SpeechItem { private final String mLanguage; private final String mCountry; private final String mVariant; public LoadLanguageItem(Object callerIdentity, int callerUid, int callerPid, - Bundle params, String language, String country, String variant) { - super(callerIdentity, callerUid, callerPid, params); + String language, String country, String variant) { + super(callerIdentity, callerUid, callerPid); mLanguage = language; mCountry = country; mVariant = variant; @@ -763,14 +1255,8 @@ public abstract class TextToSpeechService extends Service { } @Override - protected int playImpl() { - int result = TextToSpeechService.this.onLoadLanguage(mLanguage, mCountry, mVariant); - if (result == TextToSpeech.LANG_AVAILABLE || - result == TextToSpeech.LANG_COUNTRY_AVAILABLE || - result == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) { - return TextToSpeech.SUCCESS; - } - return TextToSpeech.ERROR; + protected void playImpl() { + TextToSpeechService.this.onLoadLanguage(mLanguage, mCountry, mVariant); } @Override @@ -800,7 +1286,7 @@ public abstract class TextToSpeechService extends Service { return TextToSpeech.ERROR; } - SpeechItem item = new SynthesisSpeechItem(caller, + SpeechItem item = new SynthesisSpeechItemV1(caller, Binder.getCallingUid(), Binder.getCallingPid(), params, text); return mSynthHandler.enqueueSpeechItem(queueMode, item); } @@ -818,7 +1304,7 @@ public abstract class TextToSpeechService extends Service { final ParcelFileDescriptor sameFileDescriptor = ParcelFileDescriptor.adoptFd( fileDescriptor.detachFd()); - SpeechItem item = new SynthesisToFileOutputStreamSpeechItem(caller, + SpeechItem item = new SynthesisToFileOutputStreamSpeechItemV1(caller, Binder.getCallingUid(), Binder.getCallingPid(), params, text, new ParcelFileDescriptor.AutoCloseOutputStream(sameFileDescriptor)); return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item); @@ -830,19 +1316,19 @@ public abstract class TextToSpeechService extends Service { return TextToSpeech.ERROR; } - SpeechItem item = new AudioSpeechItem(caller, + SpeechItem item = new AudioSpeechItemV1(caller, Binder.getCallingUid(), Binder.getCallingPid(), params, audioUri); return mSynthHandler.enqueueSpeechItem(queueMode, item); } @Override - public int playSilence(IBinder caller, long duration, int queueMode, Bundle params) { - if (!checkNonNull(caller, params)) { + public int playSilence(IBinder caller, long duration, int queueMode, String utteranceId) { + if (!checkNonNull(caller)) { return TextToSpeech.ERROR; } SpeechItem item = new SilenceSpeechItem(caller, - Binder.getCallingUid(), Binder.getCallingPid(), params, duration); + Binder.getCallingUid(), Binder.getCallingPid(), utteranceId, duration); return mSynthHandler.enqueueSpeechItem(queueMode, item); } @@ -912,7 +1398,7 @@ public abstract class TextToSpeechService extends Service { retVal == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) { SpeechItem item = new LoadLanguageItem(caller, Binder.getCallingUid(), - Binder.getCallingPid(), null, lang, country, variant); + Binder.getCallingPid(), lang, country, variant); if (mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item) != TextToSpeech.SUCCESS) { @@ -943,6 +1429,58 @@ public abstract class TextToSpeechService extends Service { } return true; } + + @Override + public List<VoiceInfo> getVoicesInfo() { + return TextToSpeechService.this.getVoicesInfo(); + } + + @Override + public int speakV2(IBinder callingInstance, + SynthesisRequestV2 request) { + if (!checkNonNull(callingInstance, request)) { + return TextToSpeech.ERROR; + } + + SpeechItem item = new SynthesisSpeechItemV2(callingInstance, + Binder.getCallingUid(), Binder.getCallingPid(), request); + return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item); + } + + @Override + public int synthesizeToFileDescriptorV2(IBinder callingInstance, + ParcelFileDescriptor fileDescriptor, + SynthesisRequestV2 request) { + if (!checkNonNull(callingInstance, request, fileDescriptor)) { + return TextToSpeech.ERROR; + } + + // In test env, ParcelFileDescriptor instance may be EXACTLY the same + // one that is used by client. And it will be closed by a client, thus + // preventing us from writing anything to it. + final ParcelFileDescriptor sameFileDescriptor = ParcelFileDescriptor.adoptFd( + fileDescriptor.detachFd()); + + SpeechItem item = new SynthesisToFileOutputStreamSpeechItemV2(callingInstance, + Binder.getCallingUid(), Binder.getCallingPid(), request, + new ParcelFileDescriptor.AutoCloseOutputStream(sameFileDescriptor)); + return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item); + + } + + @Override + public int playAudioV2( + IBinder callingInstance, Uri audioUri, String utteranceId, + Bundle systemParameters) { + if (!checkNonNull(callingInstance, audioUri, systemParameters)) { + return TextToSpeech.ERROR; + } + + SpeechItem item = new AudioSpeechItemV2(callingInstance, + Binder.getCallingUid(), Binder.getCallingPid(), utteranceId, systemParameters, + audioUri); + return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item); + } }; private class CallbackMap extends RemoteCallbackList<ITextToSpeechCallback> { @@ -964,11 +1502,31 @@ public abstract class TextToSpeechService extends Service { } } - public void dispatchOnDone(Object callerIdentity, String utteranceId) { + public void dispatchOnFallback(Object callerIdentity, String utteranceId) { + ITextToSpeechCallback cb = getCallbackFor(callerIdentity); + if (cb == null) return; + try { + cb.onFallback(utteranceId); + } catch (RemoteException e) { + Log.e(TAG, "Callback onFallback failed: " + e); + } + } + + public void dispatchOnStop(Object callerIdentity, String utteranceId) { + ITextToSpeechCallback cb = getCallbackFor(callerIdentity); + if (cb == null) return; + try { + cb.onStop(utteranceId); + } catch (RemoteException e) { + Log.e(TAG, "Callback onStop failed: " + e); + } + } + + public void dispatchOnSuccess(Object callerIdentity, String utteranceId) { ITextToSpeechCallback cb = getCallbackFor(callerIdentity); if (cb == null) return; try { - cb.onDone(utteranceId); + cb.onSuccess(utteranceId); } catch (RemoteException e) { Log.e(TAG, "Callback onDone failed: " + e); } @@ -985,11 +1543,12 @@ public abstract class TextToSpeechService extends Service { } - public void dispatchOnError(Object callerIdentity, String utteranceId) { + public void dispatchOnError(Object callerIdentity, String utteranceId, + int errorCode) { ITextToSpeechCallback cb = getCallbackFor(callerIdentity); if (cb == null) return; try { - cb.onError(utteranceId); + cb.onError(utteranceId, errorCode); } catch (RemoteException e) { Log.e(TAG, "Callback onError failed: " + e); } @@ -1001,7 +1560,7 @@ public abstract class TextToSpeechService extends Service { synchronized (mCallerToCallback) { mCallerToCallback.remove(caller); } - mSynthHandler.stopForApp(caller); + //mSynthHandler.stopForApp(caller); } @Override @@ -1012,6 +1571,18 @@ public abstract class TextToSpeechService extends Service { } } + public void dispatchVoicesInfoChange(List<VoiceInfo> voicesInfo) { + synchronized (mCallerToCallback) { + for (ITextToSpeechCallback callback : mCallerToCallback.values()) { + try { + callback.onVoicesInfoChange(voicesInfo); + } catch (RemoteException e) { + Log.e(TAG, "Failed to request reconnect", e); + } + } + } + } + private ITextToSpeechCallback getCallbackFor(Object caller) { ITextToSpeechCallback cb; IBinder asBinder = (IBinder) caller; @@ -1021,7 +1592,5 @@ public abstract class TextToSpeechService extends Service { return cb; } - } - } diff --git a/core/java/android/speech/tts/VoiceInfo.aidl b/core/java/android/speech/tts/VoiceInfo.aidl new file mode 100644 index 0000000..4005f8b --- /dev/null +++ b/core/java/android/speech/tts/VoiceInfo.aidl @@ -0,0 +1,20 @@ +/* +** +** Copyright 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.speech.tts; + +parcelable VoiceInfo;
\ No newline at end of file diff --git a/core/java/android/speech/tts/VoiceInfo.java b/core/java/android/speech/tts/VoiceInfo.java new file mode 100644 index 0000000..16b9a97 --- /dev/null +++ b/core/java/android/speech/tts/VoiceInfo.java @@ -0,0 +1,325 @@ +package android.speech.tts; + +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Locale; + +/** + * Characteristics and features of a Text-To-Speech Voice. Each TTS Engine can expose + * multiple voices for multiple locales, with different set of features. + * + * Each VoiceInfo has an unique name. This name can be obtained using the {@link #getName()} method + * and will persist until the client is asked to re-evaluate the list of available voices in the + * {@link TextToSpeechClient.ConnectionCallbacks#onEngineStatusChange(android.speech.tts.TextToSpeechClient.EngineStatus)} + * callback. The name can be used to reference a VoiceInfo in an instance of {@link RequestConfig}; + * the {@link TextToSpeechClient.Params#FALLBACK_VOICE_NAME} voice parameter is an example of this. + * It is recommended that the voice name never change during the TTS service lifetime. + */ +public final class VoiceInfo implements Parcelable { + /** Very low, but still intelligible quality of speech synthesis */ + public static final int QUALITY_VERY_LOW = 100; + + /** Low, not human-like quality of speech synthesis */ + public static final int QUALITY_LOW = 200; + + /** Normal quality of speech synthesis */ + public static final int QUALITY_NORMAL = 300; + + /** High, human-like quality of speech synthesis */ + public static final int QUALITY_HIGH = 400; + + /** Very high, almost human-indistinguishable quality of speech synthesis */ + public static final int QUALITY_VERY_HIGH = 500; + + /** Very low expected synthesizer latency (< 20ms) */ + public static final int LATENCY_VERY_LOW = 100; + + /** Low expected synthesizer latency (~20ms) */ + public static final int LATENCY_LOW = 200; + + /** Normal expected synthesizer latency (~50ms) */ + public static final int LATENCY_NORMAL = 300; + + /** Network based expected synthesizer latency (~200ms) */ + public static final int LATENCY_HIGH = 400; + + /** Very slow network based expected synthesizer latency (> 200ms) */ + public static final int LATENCY_VERY_HIGH = 500; + + /** Additional feature key, with string value, gender of the speaker */ + public static final String FEATURE_SPEAKER_GENDER = "speakerGender"; + + /** Additional feature key, with integer value, speaking speed in words per minute + * when {@link TextToSpeechClient.Params#SPEECH_SPEED} parameter is set to {@code 1.0} */ + public static final String FEATURE_WORDS_PER_MINUTE = "wordsPerMinute"; + + /** + * Additional feature key, with boolean value, that indicates that voice may need to + * download additional data if used for synthesis. + * + * Making a request with a voice that has this feature may result in a + * {@link TextToSpeechClient.Status#ERROR_DOWNLOADING_ADDITIONAL_DATA} error. It's recommended + * to set the {@link TextToSpeechClient.Params#FALLBACK_VOICE_NAME} voice parameter to reference + * a fully installed voice (or network voice) that can serve as replacement. + * + * Note: It's a good practice for a TTS engine to provide a sensible fallback voice as the + * default value for {@link TextToSpeechClient.Params#FALLBACK_VOICE_NAME} parameter if this + * feature is present. + */ + public static final String FEATURE_MAY_AUTOINSTALL = "mayAutoInstall"; + + private final String mName; + private final Locale mLocale; + private final int mQuality; + private final int mLatency; + private final boolean mRequiresNetworkConnection; + private final Bundle mParams; + private final Bundle mAdditionalFeatures; + + private VoiceInfo(Parcel in) { + this.mName = in.readString(); + String[] localesData = new String[3]; + in.readStringArray(localesData); + this.mLocale = new Locale(localesData[0], localesData[1], localesData[2]); + + this.mQuality = in.readInt(); + this.mLatency = in.readInt(); + this.mRequiresNetworkConnection = (in.readByte() == 1); + + this.mParams = in.readBundle(); + this.mAdditionalFeatures = in.readBundle(); + } + + private VoiceInfo(String name, + Locale locale, + int quality, + int latency, + boolean requiresNetworkConnection, + Bundle params, + Bundle additionalFeatures) { + this.mName = name; + this.mLocale = locale; + this.mQuality = quality; + this.mLatency = latency; + this.mRequiresNetworkConnection = requiresNetworkConnection; + this.mParams = params; + this.mAdditionalFeatures = additionalFeatures; + } + + /** Builder, allows TTS engines to create VoiceInfo instances. */ + public static final class Builder { + private String name; + private Locale locale; + private int quality = VoiceInfo.QUALITY_NORMAL; + private int latency = VoiceInfo.LATENCY_NORMAL; + private boolean requiresNetworkConnection; + private Bundle params; + private Bundle additionalFeatures; + + public Builder() { + + } + + /** + * Copy fields from given VoiceInfo instance. + */ + public Builder(VoiceInfo voiceInfo) { + this.name = voiceInfo.mName; + this.locale = voiceInfo.mLocale; + this.quality = voiceInfo.mQuality; + this.latency = voiceInfo.mLatency; + this.requiresNetworkConnection = voiceInfo.mRequiresNetworkConnection; + this.params = (Bundle)voiceInfo.mParams.clone(); + this.additionalFeatures = (Bundle) voiceInfo.mAdditionalFeatures.clone(); + } + + /** + * Sets the voice's unique name. It will be used by clients to reference the voice used by a + * request. + * + * It's recommended that each voice use the same consistent name during the TTS service + * lifetime. + */ + public Builder setName(String name) { + this.name = name; + return this; + } + + /** + * Sets voice locale. This has to be a valid locale, built from ISO 639-1 and ISO 3166-1 + * two letter codes. + */ + public Builder setLocale(Locale locale) { + this.locale = locale; + return this; + } + + /** + * Sets map of all available request parameters with their default values. + * Some common parameter names can be found in {@link TextToSpeechClient.Params} static + * members. + */ + public Builder setParamsWithDefaults(Bundle params) { + this.params = params; + return this; + } + + /** + * Sets map of additional voice features. Some common feature names can be found in + * {@link VoiceInfo} static members. + */ + public Builder setAdditionalFeatures(Bundle additionalFeatures) { + this.additionalFeatures = additionalFeatures; + return this; + } + + /** + * Sets the voice quality (higher is better). + */ + public Builder setQuality(int quality) { + this.quality = quality; + return this; + } + + /** + * Sets the voice latency (lower is better). + */ + public Builder setLatency(int latency) { + this.latency = latency; + return this; + } + + /** + * Sets whether the voice requires network connection to work properly. + */ + public Builder setRequiresNetworkConnection(boolean requiresNetworkConnection) { + this.requiresNetworkConnection = requiresNetworkConnection; + return this; + } + + /** + * @return The built VoiceInfo instance. + */ + public VoiceInfo build() { + if (name == null || name.isEmpty()) { + throw new IllegalStateException("Name can't be null or empty"); + } + if (locale == null) { + throw new IllegalStateException("Locale can't be null"); + } + + return new VoiceInfo(name, locale, quality, latency, + requiresNetworkConnection, + ((params == null) ? new Bundle() : + (Bundle)params.clone()), + ((additionalFeatures == null) ? new Bundle() : + (Bundle)additionalFeatures.clone())); + } + } + + /** + * @hide + */ + @Override + public int describeContents() { + return 0; + } + + /** + * @hide + */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mName); + String[] localesData = new String[]{mLocale.getLanguage(), mLocale.getCountry(), mLocale.getVariant()}; + dest.writeStringArray(localesData); + dest.writeInt(mQuality); + dest.writeInt(mLatency); + dest.writeByte((byte) (mRequiresNetworkConnection ? 1 : 0)); + dest.writeBundle(mParams); + dest.writeBundle(mAdditionalFeatures); + } + + /** + * @hide + */ + public static final Parcelable.Creator<VoiceInfo> CREATOR = new Parcelable.Creator<VoiceInfo>() { + @Override + public VoiceInfo createFromParcel(Parcel in) { + return new VoiceInfo(in); + } + + @Override + public VoiceInfo[] newArray(int size) { + return new VoiceInfo[size]; + } + }; + + /** + * @return The voice's locale + */ + public Locale getLocale() { + return mLocale; + } + + /** + * @return The voice's quality (higher is better) + */ + public int getQuality() { + return mQuality; + } + + /** + * @return The voice's latency (lower is better) + */ + public int getLatency() { + return mLatency; + } + + /** + * @return Does the Voice require a network connection to work. + */ + public boolean getRequiresNetworkConnection() { + return mRequiresNetworkConnection; + } + + /** + * @return Bundle of all available parameters with their default values. + */ + public Bundle getParamsWithDefaults() { + return mParams; + } + + /** + * @return Unique voice name. + * + * Each VoiceInfo has an unique name, that persists until client is asked to re-evaluate the + * set of the available languages in the {@link TextToSpeechClient.ConnectionCallbacks#onEngineStatusChange(android.speech.tts.TextToSpeechClient.EngineStatus)} + * callback (Voice may disappear from the set if voice was removed by the user). + */ + public String getName() { + return mName; + } + + /** + * @return Additional features of the voice. + */ + public Bundle getAdditionalFeatures() { + return mAdditionalFeatures; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(64); + return builder.append("VoiceInfo[Name: ").append(mName) + .append(" ,locale: ").append(mLocale) + .append(" ,quality: ").append(mQuality) + .append(" ,latency: ").append(mLatency) + .append(" ,requiresNetwork: ").append(mRequiresNetworkConnection) + .append(" ,paramsWithDefaults: ").append(mParams.toString()) + .append(" ,additionalFeatures: ").append(mAdditionalFeatures.toString()) + .append("]").toString(); + } +} diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java index 06935ae..77ef1da 100644 --- a/core/java/android/text/DynamicLayout.java +++ b/core/java/android/text/DynamicLayout.java @@ -21,6 +21,7 @@ import android.text.style.UpdateLayout; import android.text.style.WrapTogetherSpan; import com.android.internal.util.ArrayUtils; +import com.android.internal.util.GrowingArrayUtils; import java.lang.ref.WeakReference; @@ -401,7 +402,7 @@ public class DynamicLayout extends Layout if (mBlockEndLines == null) { // Initial creation of the array, no test on previous block ending line - mBlockEndLines = new int[ArrayUtils.idealIntArraySize(1)]; + mBlockEndLines = ArrayUtils.newUnpaddedIntArray(1); mBlockEndLines[mNumberOfBlocks] = line; mNumberOfBlocks++; return; @@ -409,13 +410,7 @@ public class DynamicLayout extends Layout final int previousBlockEndLine = mBlockEndLines[mNumberOfBlocks - 1]; if (line > previousBlockEndLine) { - if (mNumberOfBlocks == mBlockEndLines.length) { - // Grow the array if needed - int[] blockEndLines = new int[ArrayUtils.idealIntArraySize(mNumberOfBlocks + 1)]; - System.arraycopy(mBlockEndLines, 0, blockEndLines, 0, mNumberOfBlocks); - mBlockEndLines = blockEndLines; - } - mBlockEndLines[mNumberOfBlocks] = line; + mBlockEndLines = GrowingArrayUtils.append(mBlockEndLines, mNumberOfBlocks, line); mNumberOfBlocks++; } } @@ -483,9 +478,9 @@ public class DynamicLayout extends Layout } if (newNumberOfBlocks > mBlockEndLines.length) { - final int newSize = ArrayUtils.idealIntArraySize(newNumberOfBlocks); - int[] blockEndLines = new int[newSize]; - int[] blockIndices = new int[newSize]; + int[] blockEndLines = ArrayUtils.newUnpaddedIntArray( + Math.max(mBlockEndLines.length * 2, newNumberOfBlocks)); + int[] blockIndices = new int[blockEndLines.length]; System.arraycopy(mBlockEndLines, 0, blockEndLines, 0, firstBlock); System.arraycopy(mBlockIndices, 0, blockIndices, 0, firstBlock); System.arraycopy(mBlockEndLines, lastBlock + 1, diff --git a/core/java/android/text/Html.java b/core/java/android/text/Html.java index f839d52..2fcc597 100644 --- a/core/java/android/text/Html.java +++ b/core/java/android/text/Html.java @@ -48,11 +48,8 @@ import android.text.style.TypefaceSpan; import android.text.style.URLSpan; import android.text.style.UnderlineSpan; -import com.android.internal.util.XmlUtils; - import java.io.IOException; import java.io.StringReader; -import java.util.HashMap; /** * This class processes HTML strings into displayable styled text. @@ -214,7 +211,7 @@ public class Html { private static String getOpenParaTagWithDirection(Spanned text, int start, int end) { final int len = end - start; - final byte[] levels = new byte[ArrayUtils.idealByteArraySize(len)]; + final byte[] levels = ArrayUtils.newUnpaddedByteArray(len); final char[] buffer = TextUtils.obtain(len); TextUtils.getChars(text, start, end, buffer, 0); diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index 9dfd383..4bfcaff 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -31,6 +31,7 @@ import android.text.style.ReplacementSpan; import android.text.style.TabStopSpan; import com.android.internal.util.ArrayUtils; +import com.android.internal.util.GrowingArrayUtils; import java.util.Arrays; @@ -403,14 +404,9 @@ public abstract class Layout { // construction if (mLineBackgroundSpans.spanStarts[j] >= end || mLineBackgroundSpans.spanEnds[j] <= start) continue; - if (spansLength == spans.length) { - // The spans array needs to be expanded - int newSize = ArrayUtils.idealObjectArraySize(2 * spansLength); - ParagraphStyle[] newSpans = new ParagraphStyle[newSize]; - System.arraycopy(spans, 0, newSpans, 0, spansLength); - spans = newSpans; - } - spans[spansLength++] = mLineBackgroundSpans.spans[j]; + spans = GrowingArrayUtils.append( + spans, spansLength, mLineBackgroundSpans.spans[j]); + spansLength++; } } } diff --git a/core/java/android/text/MeasuredText.java b/core/java/android/text/MeasuredText.java index 101d6a2..f8e3c83 100644 --- a/core/java/android/text/MeasuredText.java +++ b/core/java/android/text/MeasuredText.java @@ -98,10 +98,10 @@ class MeasuredText { mPos = 0; if (mWidths == null || mWidths.length < len) { - mWidths = new float[ArrayUtils.idealFloatArraySize(len)]; + mWidths = ArrayUtils.newUnpaddedFloatArray(len); } if (mChars == null || mChars.length < len) { - mChars = new char[ArrayUtils.idealCharArraySize(len)]; + mChars = ArrayUtils.newUnpaddedCharArray(len); } TextUtils.getChars(text, start, end, mChars, 0); @@ -130,7 +130,7 @@ class MeasuredText { mEasy = true; } else { if (mLevels == null || mLevels.length < len) { - mLevels = new byte[ArrayUtils.idealByteArraySize(len)]; + mLevels = ArrayUtils.newUnpaddedByteArray(len); } int bidiRequest; if (textDir == TextDirectionHeuristics.LTR) { diff --git a/core/java/android/text/PackedIntVector.java b/core/java/android/text/PackedIntVector.java index d87f600..546ab44 100644 --- a/core/java/android/text/PackedIntVector.java +++ b/core/java/android/text/PackedIntVector.java @@ -17,6 +17,7 @@ package android.text; import com.android.internal.util.ArrayUtils; +import com.android.internal.util.GrowingArrayUtils; /** @@ -252,9 +253,9 @@ class PackedIntVector { */ private final void growBuffer() { final int columns = mColumns; - int newsize = size() + 1; - newsize = ArrayUtils.idealIntArraySize(newsize * columns) / columns; - int[] newvalues = new int[newsize * columns]; + int[] newvalues = ArrayUtils.newUnpaddedIntArray( + GrowingArrayUtils.growSize(size()) * columns); + int newsize = newvalues.length / columns; final int[] valuegap = mValueGap; final int rowgapstart = mRowGapStart; diff --git a/core/java/android/text/PackedObjectVector.java b/core/java/android/text/PackedObjectVector.java index a29df09..b777e16 100644 --- a/core/java/android/text/PackedObjectVector.java +++ b/core/java/android/text/PackedObjectVector.java @@ -17,6 +17,9 @@ package android.text; import com.android.internal.util.ArrayUtils; +import com.android.internal.util.GrowingArrayUtils; + +import libcore.util.EmptyArray; class PackedObjectVector<E> { @@ -32,12 +35,11 @@ class PackedObjectVector<E> PackedObjectVector(int columns) { mColumns = columns; - mRows = ArrayUtils.idealIntArraySize(0) / mColumns; + mValues = EmptyArray.OBJECT; + mRows = 0; mRowGapStart = 0; mRowGapLength = mRows; - - mValues = new Object[mRows * mColumns]; } public E @@ -109,10 +111,9 @@ class PackedObjectVector<E> private void growBuffer() { - int newsize = size() + 1; - newsize = ArrayUtils.idealIntArraySize(newsize * mColumns) / mColumns; - Object[] newvalues = new Object[newsize * mColumns]; - + Object[] newvalues = ArrayUtils.newUnpaddedObjectArray( + GrowingArrayUtils.growSize(size()) * mColumns); + int newsize = newvalues.length / mColumns; int after = mRows - (mRowGapStart + mRowGapLength); System.arraycopy(mValues, 0, newvalues, 0, mColumns * mRowGapStart); diff --git a/core/java/android/text/SpannableStringBuilder.java b/core/java/android/text/SpannableStringBuilder.java index 34274a6..f440853 100644 --- a/core/java/android/text/SpannableStringBuilder.java +++ b/core/java/android/text/SpannableStringBuilder.java @@ -21,6 +21,9 @@ import android.graphics.Paint; import android.util.Log; import com.android.internal.util.ArrayUtils; +import com.android.internal.util.GrowingArrayUtils; + +import libcore.util.EmptyArray; import java.lang.reflect.Array; @@ -29,6 +32,7 @@ import java.lang.reflect.Array; */ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable, Editable, Appendable, GraphicsOperations { + private final static String TAG = "SpannableStringBuilder"; /** * Create a new SpannableStringBuilder with empty contents */ @@ -53,19 +57,17 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable if (srclen < 0) throw new StringIndexOutOfBoundsException(); - int len = ArrayUtils.idealCharArraySize(srclen + 1); - mText = new char[len]; + mText = ArrayUtils.newUnpaddedCharArray(GrowingArrayUtils.growSize(srclen)); mGapStart = srclen; - mGapLength = len - srclen; + mGapLength = mText.length - srclen; TextUtils.getChars(text, start, end, mText, 0); mSpanCount = 0; - int alloc = ArrayUtils.idealIntArraySize(0); - mSpans = new Object[alloc]; - mSpanStarts = new int[alloc]; - mSpanEnds = new int[alloc]; - mSpanFlags = new int[alloc]; + mSpans = EmptyArray.OBJECT; + mSpanStarts = EmptyArray.INT; + mSpanEnds = EmptyArray.INT; + mSpanFlags = EmptyArray.INT; if (text instanceof Spanned) { Spanned sp = (Spanned) text; @@ -129,12 +131,14 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable private void resizeFor(int size) { final int oldLength = mText.length; - final int newLength = ArrayUtils.idealCharArraySize(size + 1); - final int delta = newLength - oldLength; - if (delta == 0) return; + if (size + 1 <= oldLength) { + return; + } - char[] newText = new char[newLength]; + char[] newText = ArrayUtils.newUnpaddedCharArray(GrowingArrayUtils.growSize(size)); System.arraycopy(mText, 0, newText, 0, mGapStart); + final int newLength = newText.length; + final int delta = newLength - oldLength; final int after = oldLength - (mGapStart + mGapLength); System.arraycopy(mText, oldLength - after, newText, newLength - after, after); mText = newText; @@ -436,10 +440,26 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable } // Documentation from interface - public SpannableStringBuilder replace(final int start, final int end, + public SpannableStringBuilder replace(int start, int end, CharSequence tb, int tbstart, int tbend) { checkRange("replace", start, end); + // Sanity check + if (start > end) { + Log.w(TAG, "Bad arguments to #replace : " + + "start = " + start + ", end = " + end); + final int tmp = start; + start = end; + end = tmp; + } + if (tbstart > tbend) { + Log.w(TAG, "Bad arguments to #replace : " + + "tbstart = " + tbstart + ", tbend = " + tbend); + final int tmp = tbstart; + tbstart = tbend; + tbend = tmp; + } + int filtercount = mFilters.length; for (int i = 0; i < filtercount; i++) { CharSequence repl = mFilters[i].filter(tb, tbstart, tbend, this, start, end); @@ -613,8 +633,9 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable // 0-length Spanned.SPAN_EXCLUSIVE_EXCLUSIVE if (flagsStart == POINT && flagsEnd == MARK && start == end) { - if (send) Log.e("SpannableStringBuilder", - "SPAN_EXCLUSIVE_EXCLUSIVE spans cannot have a zero length"); + if (send) { + Log.e(TAG, "SPAN_EXCLUSIVE_EXCLUSIVE spans cannot have a zero length"); + } // Silently ignore invalid spans when they are created from this class. // This avoids the duplication of the above test code before all the // calls to setSpan that are done in this class @@ -661,28 +682,10 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable } } - if (mSpanCount + 1 >= mSpans.length) { - int newsize = ArrayUtils.idealIntArraySize(mSpanCount + 1); - Object[] newspans = new Object[newsize]; - int[] newspanstarts = new int[newsize]; - int[] newspanends = new int[newsize]; - int[] newspanflags = new int[newsize]; - - System.arraycopy(mSpans, 0, newspans, 0, mSpanCount); - System.arraycopy(mSpanStarts, 0, newspanstarts, 0, mSpanCount); - System.arraycopy(mSpanEnds, 0, newspanends, 0, mSpanCount); - System.arraycopy(mSpanFlags, 0, newspanflags, 0, mSpanCount); - - mSpans = newspans; - mSpanStarts = newspanstarts; - mSpanEnds = newspanends; - mSpanFlags = newspanflags; - } - - mSpans[mSpanCount] = what; - mSpanStarts[mSpanCount] = start; - mSpanEnds[mSpanCount] = end; - mSpanFlags[mSpanCount] = flags; + mSpans = GrowingArrayUtils.append(mSpans, mSpanCount, what); + mSpanStarts = GrowingArrayUtils.append(mSpanStarts, mSpanCount, start); + mSpanEnds = GrowingArrayUtils.append(mSpanEnds, mSpanCount, end); + mSpanFlags = GrowingArrayUtils.append(mSpanFlags, mSpanCount, flags); mSpanCount++; if (send) sendSpanAdded(what, nstart, nend); diff --git a/core/java/android/text/SpannableStringInternal.java b/core/java/android/text/SpannableStringInternal.java index 456a3e5..d114d32 100644 --- a/core/java/android/text/SpannableStringInternal.java +++ b/core/java/android/text/SpannableStringInternal.java @@ -17,6 +17,9 @@ package android.text; import com.android.internal.util.ArrayUtils; +import com.android.internal.util.GrowingArrayUtils; + +import libcore.util.EmptyArray; import java.lang.reflect.Array; @@ -29,9 +32,8 @@ import java.lang.reflect.Array; else mText = source.toString().substring(start, end); - int initial = ArrayUtils.idealIntArraySize(0); - mSpans = new Object[initial]; - mSpanData = new int[initial * 3]; + mSpans = EmptyArray.OBJECT; + mSpanData = EmptyArray.INT; if (source instanceof Spanned) { Spanned sp = (Spanned) source; @@ -115,9 +117,9 @@ import java.lang.reflect.Array; } if (mSpanCount + 1 >= mSpans.length) { - int newsize = ArrayUtils.idealIntArraySize(mSpanCount + 1); - Object[] newtags = new Object[newsize]; - int[] newdata = new int[newsize * 3]; + Object[] newtags = ArrayUtils.newUnpaddedObjectArray( + GrowingArrayUtils.growSize(mSpanCount)); + int[] newdata = new int[newtags.length * 3]; System.arraycopy(mSpans, 0, newtags, 0, mSpanCount); System.arraycopy(mSpanData, 0, newdata, 0, mSpanCount * 3); diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java index e7d6fda..638ef22 100644 --- a/core/java/android/text/StaticLayout.java +++ b/core/java/android/text/StaticLayout.java @@ -26,6 +26,7 @@ import android.text.style.TabStopSpan; import android.util.Log; import com.android.internal.util.ArrayUtils; +import com.android.internal.util.GrowingArrayUtils; /** * StaticLayout is a Layout for text that will not be edited after it @@ -130,9 +131,8 @@ public class StaticLayout extends Layout { mEllipsizedWidth = outerwidth; } - mLines = new int[ArrayUtils.idealIntArraySize(2 * mColumns)]; - mLineDirections = new Directions[ - ArrayUtils.idealIntArraySize(2 * mColumns)]; + mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2 * mColumns); + mLines = new int[mLineDirections.length]; mMaximumVisibleLineCount = maxLines; mMeasured = MeasuredText.obtain(); @@ -149,8 +149,8 @@ public class StaticLayout extends Layout { super(text, null, 0, null, 0, 0); mColumns = COLUMNS_ELLIPSIZE; - mLines = new int[ArrayUtils.idealIntArraySize(2 * mColumns)]; - mLineDirections = new Directions[ArrayUtils.idealIntArraySize(2 * mColumns)]; + mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2 * mColumns); + mLines = new int[mLineDirections.length]; // FIXME This is never recycled mMeasured = MeasuredText.obtain(); } @@ -215,8 +215,7 @@ public class StaticLayout extends Layout { if (chooseHt.length != 0) { if (chooseHtv == null || chooseHtv.length < chooseHt.length) { - chooseHtv = new int[ArrayUtils.idealIntArraySize( - chooseHt.length)]; + chooseHtv = ArrayUtils.newUnpaddedIntArray(chooseHt.length); } for (int i = 0; i < chooseHt.length; i++) { @@ -434,7 +433,7 @@ public class StaticLayout extends Layout { } if (mLineCount >= mMaximumVisibleLineCount) { - break; + return; } } } @@ -599,16 +598,16 @@ public class StaticLayout extends Layout { int[] lines = mLines; if (want >= lines.length) { - int nlen = ArrayUtils.idealIntArraySize(want + 1); - int[] grow = new int[nlen]; - System.arraycopy(lines, 0, grow, 0, lines.length); - mLines = grow; - lines = grow; - - Directions[] grow2 = new Directions[nlen]; + Directions[] grow2 = ArrayUtils.newUnpaddedArray( + Directions.class, GrowingArrayUtils.growSize(want)); System.arraycopy(mLineDirections, 0, grow2, 0, mLineDirections.length); mLineDirections = grow2; + + int[] grow = new int[grow2.length]; + System.arraycopy(lines, 0, grow, 0, lines.length); + mLines = grow; + lines = grow; } if (chooseHt != null) { diff --git a/core/java/android/text/TextLine.java b/core/java/android/text/TextLine.java index 1fecf81..d892f19 100644 --- a/core/java/android/text/TextLine.java +++ b/core/java/android/text/TextLine.java @@ -153,7 +153,7 @@ class TextLine { if (mCharsValid) { if (mChars == null || mChars.length < mLen) { - mChars = new char[ArrayUtils.idealCharArraySize(mLen)]; + mChars = ArrayUtils.newUnpaddedCharArray(mLen); } TextUtils.getChars(text, start, limit, mChars, 0); if (hasReplacement) { diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java index 596ca8c..f06ae71 100644 --- a/core/java/android/text/TextUtils.java +++ b/core/java/android/text/TextUtils.java @@ -1321,7 +1321,7 @@ public class TextUtils { } if (buf == null || buf.length < len) - buf = new char[ArrayUtils.idealCharArraySize(len)]; + buf = ArrayUtils.newUnpaddedCharArray(len); return buf; } diff --git a/core/java/android/text/format/DateUtils.java b/core/java/android/text/format/DateUtils.java index 22675b4..d0ed871 100644 --- a/core/java/android/text/format/DateUtils.java +++ b/core/java/android/text/format/DateUtils.java @@ -28,7 +28,6 @@ import java.util.Date; import java.util.Formatter; import java.util.GregorianCalendar; import java.util.Locale; -import java.util.TimeZone; import libcore.icu.DateIntervalFormat; import libcore.icu.LocaleData; diff --git a/core/java/android/text/method/HideReturnsTransformationMethod.java b/core/java/android/text/method/HideReturnsTransformationMethod.java index ce18692..c6a90ca 100644 --- a/core/java/android/text/method/HideReturnsTransformationMethod.java +++ b/core/java/android/text/method/HideReturnsTransformationMethod.java @@ -16,13 +16,6 @@ package android.text.method; -import android.graphics.Rect; -import android.text.GetChars; -import android.text.Spanned; -import android.text.SpannedString; -import android.text.TextUtils; -import android.view.View; - /** * This transformation method causes any carriage return characters (\r) * to be hidden by displaying them as zero-width non-breaking space diff --git a/core/java/android/text/method/PasswordTransformationMethod.java b/core/java/android/text/method/PasswordTransformationMethod.java index b769b76..88a69b9 100644 --- a/core/java/android/text/method/PasswordTransformationMethod.java +++ b/core/java/android/text/method/PasswordTransformationMethod.java @@ -25,7 +25,6 @@ import android.text.GetChars; import android.text.NoCopySpan; import android.text.TextUtils; import android.text.TextWatcher; -import android.text.Selection; import android.text.Spanned; import android.text.Spannable; import android.text.style.UpdateLayout; diff --git a/core/java/android/text/method/SingleLineTransformationMethod.java b/core/java/android/text/method/SingleLineTransformationMethod.java index 6a05fe4..818526a 100644 --- a/core/java/android/text/method/SingleLineTransformationMethod.java +++ b/core/java/android/text/method/SingleLineTransformationMethod.java @@ -16,15 +16,6 @@ package android.text.method; -import android.graphics.Rect; -import android.text.Editable; -import android.text.GetChars; -import android.text.Spannable; -import android.text.Spanned; -import android.text.SpannedString; -import android.text.TextUtils; -import android.view.View; - /** * This transformation method causes any newline characters (\n) to be * displayed as spaces instead of causing line breaks, and causes diff --git a/core/java/android/text/style/BackgroundColorSpan.java b/core/java/android/text/style/BackgroundColorSpan.java index 580a369..cda8015 100644 --- a/core/java/android/text/style/BackgroundColorSpan.java +++ b/core/java/android/text/style/BackgroundColorSpan.java @@ -26,9 +26,9 @@ public class BackgroundColorSpan extends CharacterStyle private final int mColor; - public BackgroundColorSpan(int color) { - mColor = color; - } + public BackgroundColorSpan(int color) { + mColor = color; + } public BackgroundColorSpan(Parcel src) { mColor = src.readInt(); @@ -46,12 +46,12 @@ public class BackgroundColorSpan extends CharacterStyle dest.writeInt(mColor); } - public int getBackgroundColor() { - return mColor; - } + public int getBackgroundColor() { + return mColor; + } - @Override - public void updateDrawState(TextPaint ds) { - ds.bgColor = mColor; - } + @Override + public void updateDrawState(TextPaint ds) { + ds.bgColor = mColor; + } } diff --git a/core/java/android/text/style/CharacterStyle.java b/core/java/android/text/style/CharacterStyle.java index 14dfddd..5b95f1a 100644 --- a/core/java/android/text/style/CharacterStyle.java +++ b/core/java/android/text/style/CharacterStyle.java @@ -24,7 +24,7 @@ import android.text.TextPaint; * ones may just implement {@link UpdateAppearance}. */ public abstract class CharacterStyle { - public abstract void updateDrawState(TextPaint tp); + public abstract void updateDrawState(TextPaint tp); /** * A given CharacterStyle can only applied to a single region of a given diff --git a/core/java/android/text/style/DrawableMarginSpan.java b/core/java/android/text/style/DrawableMarginSpan.java index c2564d5..20b6886 100644 --- a/core/java/android/text/style/DrawableMarginSpan.java +++ b/core/java/android/text/style/DrawableMarginSpan.java @@ -19,7 +19,6 @@ package android.text.style; import android.graphics.drawable.Drawable; import android.graphics.Paint; import android.graphics.Canvas; -import android.graphics.RectF; import android.text.Spanned; import android.text.Layout; diff --git a/core/java/android/text/style/DynamicDrawableSpan.java b/core/java/android/text/style/DynamicDrawableSpan.java index 89dc45b..5b8a6dd 100644 --- a/core/java/android/text/style/DynamicDrawableSpan.java +++ b/core/java/android/text/style/DynamicDrawableSpan.java @@ -17,12 +17,9 @@ package android.text.style; import android.graphics.Canvas; -import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; -import android.graphics.Paint.Style; import android.graphics.drawable.Drawable; -import android.util.Log; import java.lang.ref.WeakReference; diff --git a/core/java/android/text/style/ForegroundColorSpan.java b/core/java/android/text/style/ForegroundColorSpan.java index 476124d..c9e09bd 100644 --- a/core/java/android/text/style/ForegroundColorSpan.java +++ b/core/java/android/text/style/ForegroundColorSpan.java @@ -26,9 +26,9 @@ public class ForegroundColorSpan extends CharacterStyle private final int mColor; - public ForegroundColorSpan(int color) { - mColor = color; - } + public ForegroundColorSpan(int color) { + mColor = color; + } public ForegroundColorSpan(Parcel src) { mColor = src.readInt(); @@ -46,12 +46,12 @@ public class ForegroundColorSpan extends CharacterStyle dest.writeInt(mColor); } - public int getForegroundColor() { - return mColor; - } + public int getForegroundColor() { + return mColor; + } - @Override - public void updateDrawState(TextPaint ds) { - ds.setColor(mColor); - } + @Override + public void updateDrawState(TextPaint ds) { + ds.setColor(mColor); + } } diff --git a/core/java/android/text/style/IconMarginSpan.java b/core/java/android/text/style/IconMarginSpan.java index c786a17..cf9a705 100644 --- a/core/java/android/text/style/IconMarginSpan.java +++ b/core/java/android/text/style/IconMarginSpan.java @@ -19,7 +19,6 @@ package android.text.style; import android.graphics.Paint; import android.graphics.Bitmap; import android.graphics.Canvas; -import android.graphics.RectF; import android.text.Spanned; import android.text.Layout; diff --git a/core/java/android/text/style/ImageSpan.java b/core/java/android/text/style/ImageSpan.java index 74b9463..3d6f8e6 100644 --- a/core/java/android/text/style/ImageSpan.java +++ b/core/java/android/text/style/ImageSpan.java @@ -145,7 +145,7 @@ public class ImageSpan extends DynamicDrawableSpan { } } else { try { - drawable = mContext.getResources().getDrawable(mResourceId); + drawable = mContext.getDrawable(mResourceId); drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); } catch (Exception e) { diff --git a/core/java/android/text/style/LineHeightSpan.java b/core/java/android/text/style/LineHeightSpan.java index 44a1706..1ebee82 100644 --- a/core/java/android/text/style/LineHeightSpan.java +++ b/core/java/android/text/style/LineHeightSpan.java @@ -17,8 +17,6 @@ package android.text.style; import android.graphics.Paint; -import android.graphics.Canvas; -import android.text.Layout; import android.text.TextPaint; public interface LineHeightSpan diff --git a/core/java/android/text/style/MaskFilterSpan.java b/core/java/android/text/style/MaskFilterSpan.java index 64ab0d8..2ff52a8 100644 --- a/core/java/android/text/style/MaskFilterSpan.java +++ b/core/java/android/text/style/MaskFilterSpan.java @@ -21,18 +21,18 @@ import android.text.TextPaint; public class MaskFilterSpan extends CharacterStyle implements UpdateAppearance { - private MaskFilter mFilter; + private MaskFilter mFilter; - public MaskFilterSpan(MaskFilter filter) { - mFilter = filter; - } + public MaskFilterSpan(MaskFilter filter) { + mFilter = filter; + } - public MaskFilter getMaskFilter() { - return mFilter; - } + public MaskFilter getMaskFilter() { + return mFilter; + } - @Override - public void updateDrawState(TextPaint ds) { - ds.setMaskFilter(mFilter); - } + @Override + public void updateDrawState(TextPaint ds) { + ds.setMaskFilter(mFilter); + } } diff --git a/core/java/android/text/style/MetricAffectingSpan.java b/core/java/android/text/style/MetricAffectingSpan.java index 92558eb..853ecc6 100644 --- a/core/java/android/text/style/MetricAffectingSpan.java +++ b/core/java/android/text/style/MetricAffectingSpan.java @@ -16,7 +16,6 @@ package android.text.style; -import android.graphics.Paint; import android.text.TextPaint; /** @@ -27,7 +26,7 @@ public abstract class MetricAffectingSpan extends CharacterStyle implements UpdateLayout { - public abstract void updateMeasureState(TextPaint p); + public abstract void updateMeasureState(TextPaint p); /** * Returns "this" for most MetricAffectingSpans, but for diff --git a/core/java/android/text/style/RasterizerSpan.java b/core/java/android/text/style/RasterizerSpan.java index 75b5bcc..cae9640 100644 --- a/core/java/android/text/style/RasterizerSpan.java +++ b/core/java/android/text/style/RasterizerSpan.java @@ -21,18 +21,18 @@ import android.text.TextPaint; public class RasterizerSpan extends CharacterStyle implements UpdateAppearance { - private Rasterizer mRasterizer; + private Rasterizer mRasterizer; - public RasterizerSpan(Rasterizer r) { - mRasterizer = r; - } + public RasterizerSpan(Rasterizer r) { + mRasterizer = r; + } - public Rasterizer getRasterizer() { - return mRasterizer; - } + public Rasterizer getRasterizer() { + return mRasterizer; + } - @Override - public void updateDrawState(TextPaint ds) { - ds.setRasterizer(mRasterizer); - } + @Override + public void updateDrawState(TextPaint ds) { + ds.setRasterizer(mRasterizer); + } } diff --git a/core/java/android/text/style/RelativeSizeSpan.java b/core/java/android/text/style/RelativeSizeSpan.java index 9717362..632dbd4 100644 --- a/core/java/android/text/style/RelativeSizeSpan.java +++ b/core/java/android/text/style/RelativeSizeSpan.java @@ -23,11 +23,11 @@ import android.text.TextUtils; public class RelativeSizeSpan extends MetricAffectingSpan implements ParcelableSpan { - private final float mProportion; + private final float mProportion; - public RelativeSizeSpan(float proportion) { - mProportion = proportion; - } + public RelativeSizeSpan(float proportion) { + mProportion = proportion; + } public RelativeSizeSpan(Parcel src) { mProportion = src.readFloat(); @@ -45,17 +45,17 @@ public class RelativeSizeSpan extends MetricAffectingSpan implements ParcelableS dest.writeFloat(mProportion); } - public float getSizeChange() { - return mProportion; - } + public float getSizeChange() { + return mProportion; + } - @Override - public void updateDrawState(TextPaint ds) { - ds.setTextSize(ds.getTextSize() * mProportion); - } + @Override + public void updateDrawState(TextPaint ds) { + ds.setTextSize(ds.getTextSize() * mProportion); + } - @Override - public void updateMeasureState(TextPaint ds) { - ds.setTextSize(ds.getTextSize() * mProportion); - } + @Override + public void updateMeasureState(TextPaint ds) { + ds.setTextSize(ds.getTextSize() * mProportion); + } } diff --git a/core/java/android/text/style/ScaleXSpan.java b/core/java/android/text/style/ScaleXSpan.java index 655064b..a22a5a1 100644 --- a/core/java/android/text/style/ScaleXSpan.java +++ b/core/java/android/text/style/ScaleXSpan.java @@ -23,11 +23,11 @@ import android.text.TextUtils; public class ScaleXSpan extends MetricAffectingSpan implements ParcelableSpan { - private final float mProportion; + private final float mProportion; - public ScaleXSpan(float proportion) { - mProportion = proportion; - } + public ScaleXSpan(float proportion) { + mProportion = proportion; + } public ScaleXSpan(Parcel src) { mProportion = src.readFloat(); @@ -45,17 +45,17 @@ public class ScaleXSpan extends MetricAffectingSpan implements ParcelableSpan { dest.writeFloat(mProportion); } - public float getScaleX() { - return mProportion; - } + public float getScaleX() { + return mProportion; + } - @Override - public void updateDrawState(TextPaint ds) { - ds.setTextScaleX(ds.getTextScaleX() * mProportion); - } + @Override + public void updateDrawState(TextPaint ds) { + ds.setTextScaleX(ds.getTextScaleX() * mProportion); + } - @Override - public void updateMeasureState(TextPaint ds) { - ds.setTextScaleX(ds.getTextScaleX() * mProportion); - } + @Override + public void updateMeasureState(TextPaint ds) { + ds.setTextScaleX(ds.getTextScaleX() * mProportion); + } } diff --git a/core/java/android/text/style/StrikethroughSpan.java b/core/java/android/text/style/StrikethroughSpan.java index b51363a..303e415 100644 --- a/core/java/android/text/style/StrikethroughSpan.java +++ b/core/java/android/text/style/StrikethroughSpan.java @@ -40,8 +40,8 @@ public class StrikethroughSpan extends CharacterStyle public void writeToParcel(Parcel dest, int flags) { } - @Override - public void updateDrawState(TextPaint ds) { - ds.setStrikeThruText(true); - } + @Override + public void updateDrawState(TextPaint ds) { + ds.setStrikeThruText(true); + } } diff --git a/core/java/android/text/style/StyleSpan.java b/core/java/android/text/style/StyleSpan.java index 8e6147c..b08f70e 100644 --- a/core/java/android/text/style/StyleSpan.java +++ b/core/java/android/text/style/StyleSpan.java @@ -33,17 +33,17 @@ import android.text.TextUtils; */ public class StyleSpan extends MetricAffectingSpan implements ParcelableSpan { - private final int mStyle; - - /** - * - * @param style An integer constant describing the style for this span. Examples - * include bold, italic, and normal. Values are constants defined - * in {@link android.graphics.Typeface}. - */ - public StyleSpan(int style) { - mStyle = style; - } + private final int mStyle; + + /** + * + * @param style An integer constant describing the style for this span. Examples + * include bold, italic, and normal. Values are constants defined + * in {@link android.graphics.Typeface}. + */ + public StyleSpan(int style) { + mStyle = style; + } public StyleSpan(Parcel src) { mStyle = src.readInt(); @@ -61,19 +61,19 @@ public class StyleSpan extends MetricAffectingSpan implements ParcelableSpan { dest.writeInt(mStyle); } - /** - * Returns the style constant defined in {@link android.graphics.Typeface}. - */ - public int getStyle() { - return mStyle; - } + /** + * Returns the style constant defined in {@link android.graphics.Typeface}. + */ + public int getStyle() { + return mStyle; + } - @Override + @Override public void updateDrawState(TextPaint ds) { apply(ds, mStyle); } - @Override + @Override public void updateMeasureState(TextPaint paint) { apply(paint, mStyle); } diff --git a/core/java/android/text/style/SuggestionSpan.java b/core/java/android/text/style/SuggestionSpan.java index 0ec7e84..8b40953 100644 --- a/core/java/android/text/style/SuggestionSpan.java +++ b/core/java/android/text/style/SuggestionSpan.java @@ -166,25 +166,25 @@ public class SuggestionSpan extends CharacterStyle implements ParcelableSpan { return; } - int defStyle = com.android.internal.R.attr.textAppearanceMisspelledSuggestion; + int defStyleAttr = com.android.internal.R.attr.textAppearanceMisspelledSuggestion; TypedArray typedArray = context.obtainStyledAttributes( - null, com.android.internal.R.styleable.SuggestionSpan, defStyle, 0); + null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0); mMisspelledUnderlineThickness = typedArray.getDimension( com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0); mMisspelledUnderlineColor = typedArray.getColor( com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK); - defStyle = com.android.internal.R.attr.textAppearanceEasyCorrectSuggestion; + defStyleAttr = com.android.internal.R.attr.textAppearanceEasyCorrectSuggestion; typedArray = context.obtainStyledAttributes( - null, com.android.internal.R.styleable.SuggestionSpan, defStyle, 0); + null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0); mEasyCorrectUnderlineThickness = typedArray.getDimension( com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0); mEasyCorrectUnderlineColor = typedArray.getColor( com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK); - defStyle = com.android.internal.R.attr.textAppearanceAutoCorrectionSuggestion; + defStyleAttr = com.android.internal.R.attr.textAppearanceAutoCorrectionSuggestion; typedArray = context.obtainStyledAttributes( - null, com.android.internal.R.styleable.SuggestionSpan, defStyle, 0); + null, com.android.internal.R.styleable.SuggestionSpan, defStyleAttr, 0); mAutoCorrectionUnderlineThickness = typedArray.getDimension( com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0); mAutoCorrectionUnderlineColor = typedArray.getColor( diff --git a/core/java/android/text/style/UnderlineSpan.java b/core/java/android/text/style/UnderlineSpan.java index b0cb0e8..80b2427 100644 --- a/core/java/android/text/style/UnderlineSpan.java +++ b/core/java/android/text/style/UnderlineSpan.java @@ -40,8 +40,8 @@ public class UnderlineSpan extends CharacterStyle public void writeToParcel(Parcel dest, int flags) { } - @Override - public void updateDrawState(TextPaint ds) { - ds.setUnderlineText(true); - } + @Override + public void updateDrawState(TextPaint ds) { + ds.setUnderlineText(true); + } } diff --git a/core/java/android/transition/CircularPropagation.java b/core/java/android/transition/CircularPropagation.java new file mode 100644 index 0000000..18a3d22 --- /dev/null +++ b/core/java/android/transition/CircularPropagation.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2014 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.transition; + +import android.graphics.Rect; +import android.util.FloatMath; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +/** + * A propagation that varies with the distance to the epicenter of the Transition + * or center of the scene if no epicenter exists. When a View is visible in the + * start of the transition, Views farther from the epicenter will transition + * sooner than Views closer to the epicenter. When a View is not in the start + * of the transition or is not visible at the start of the transition, it will + * transition sooner when closer to the epicenter and later when farther from + * the epicenter. This is the default TransitionPropagation used with + * {@link android.transition.Explode}. + */ +public class CircularPropagation extends VisibilityPropagation { + private static final String TAG = "CircularPropagation"; + + private float mPropagationSpeed = 4.0f; + + /** + * Sets the speed at which transition propagation happens, relative to the duration of the + * Transition. A <code>propagationSpeed</code> of 1 means that a View centered farthest from + * the epicenter and View centered at the epicenter will have a difference + * in start delay of approximately the duration of the Transition. A speed of 2 means the + * start delay difference will be approximately half of the duration of the transition. A + * value of 0 is illegal, but negative values will invert the propagation. + * + * @param propagationSpeed The speed at which propagation occurs, relative to the duration + * of the transition. A speed of 4 means it works 4 times as fast + * as the duration of the transition. May not be 0. + */ + public void setPropagationSpeed(float propagationSpeed) { + if (propagationSpeed == 0) { + throw new IllegalArgumentException("propagationSpeed may not be 0"); + } + mPropagationSpeed = propagationSpeed; + } + + @Override + public long getStartDelay(ViewGroup sceneRoot, Transition transition, + TransitionValues startValues, TransitionValues endValues) { + if (startValues == null && endValues == null) { + return 0; + } + int directionMultiplier = 1; + TransitionValues positionValues; + if (endValues == null || getViewVisibility(startValues) == View.VISIBLE) { + positionValues = startValues; + directionMultiplier = -1; + } else { + positionValues = endValues; + } + + int viewCenterX = getViewX(positionValues); + int viewCenterY = getViewY(positionValues); + + Rect epicenter = transition.getEpicenter(); + int epicenterX; + int epicenterY; + if (epicenter != null) { + epicenterX = epicenter.centerX(); + epicenterY = epicenter.centerY(); + } else { + int[] loc = new int[2]; + sceneRoot.getLocationOnScreen(loc); + epicenterX = Math.round(loc[0] + (sceneRoot.getWidth() / 2) + + sceneRoot.getTranslationX()); + epicenterY = Math.round(loc[1] + (sceneRoot.getHeight() / 2) + + sceneRoot.getTranslationY()); + } + float distance = distance(viewCenterX, viewCenterY, epicenterX, epicenterY); + float maxDistance = distance(0, 0, sceneRoot.getWidth(), sceneRoot.getHeight()); + float distanceFraction = distance/maxDistance; + + return Math.round(transition.getDuration() * directionMultiplier / mPropagationSpeed + * distanceFraction); + } + + private static float distance(float x1, float y1, float x2, float y2) { + float x = x2 - x1; + float y = y2 - y1; + return FloatMath.sqrt((x * x) + (y * y)); + } +} diff --git a/core/java/android/transition/Explode.java b/core/java/android/transition/Explode.java new file mode 100644 index 0000000..fae527c --- /dev/null +++ b/core/java/android/transition/Explode.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2014 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.transition; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; +import android.graphics.Path; +import android.graphics.Rect; +import android.util.FloatMath; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; + +/** + * This transition tracks changes to the visibility of target views in the + * start and end scenes and moves views in or out from the edges of the + * scene. Visibility is determined by both the + * {@link View#setVisibility(int)} state of the view as well as whether it + * is parented in the current view hierarchy. Disappearing Views are + * limited as described in {@link Visibility#onDisappear(android.view.ViewGroup, + * TransitionValues, int, TransitionValues, int)}. + * <p>Views move away from the focal View or the center of the Scene if + * no epicenter was provided.</p> + */ +public class Explode extends Visibility { + private static final TimeInterpolator sDecelerate = new DecelerateInterpolator(); + private static final TimeInterpolator sAccelerate = new AccelerateInterpolator(); + private static final String TAG = "Explode"; + + private static final String PROPNAME_SCREEN_BOUNDS = "android:out:screenBounds"; + + private int[] mTempLoc = new int[2]; + + public Explode() { + setPropagation(new CircularPropagation()); + } + + private void captureValues(TransitionValues transitionValues) { + View view = transitionValues.view; + view.getLocationOnScreen(mTempLoc); + int left = mTempLoc[0] + Math.round(view.getTranslationX()); + int top = mTempLoc[1] + Math.round(view.getTranslationY()); + int right = left + view.getWidth(); + int bottom = top + view.getHeight(); + transitionValues.values.put(PROPNAME_SCREEN_BOUNDS, new Rect(left, top, right, bottom)); + } + + @Override + public void captureStartValues(TransitionValues transitionValues) { + super.captureStartValues(transitionValues); + captureValues(transitionValues); + } + + @Override + public void captureEndValues(TransitionValues transitionValues) { + super.captureEndValues(transitionValues); + captureValues(transitionValues); + } + + private Animator createAnimation(final View view, float startX, float startY, float endX, + float endY, float terminalX, float terminalY, TimeInterpolator interpolator) { + view.setTranslationX(startX); + view.setTranslationY(startY); + if (startY == endY && startX == endX) { + return null; + } + Path path = new Path(); + path.moveTo(startX, startY); + path.lineTo(endX, endY); + ObjectAnimator pathAnimator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, + View.TRANSLATION_Y, path); + pathAnimator.setInterpolator(interpolator); + OutAnimatorListener listener = new OutAnimatorListener(view, terminalX, terminalY, + endX, endY); + pathAnimator.addListener(listener); + pathAnimator.addPauseListener(listener); + + return pathAnimator; + } + + @Override + public Animator onAppear(ViewGroup sceneRoot, View view, + TransitionValues startValues, TransitionValues endValues) { + if (endValues == null) { + return null; + } + Rect bounds = (Rect) endValues.values.get(PROPNAME_SCREEN_BOUNDS); + calculateOut(sceneRoot, bounds, mTempLoc); + + final float endX = view.getTranslationX(); + final float startX = endX + mTempLoc[0]; + final float endY = view.getTranslationY(); + final float startY = endY + mTempLoc[1]; + + return createAnimation(view, startX, startY, endX, endY, endX, endY, sDecelerate); + } + + @Override + public Animator onDisappear(ViewGroup sceneRoot, View view, + TransitionValues startValues, TransitionValues endValues) { + Rect bounds = (Rect) startValues.values.get(PROPNAME_SCREEN_BOUNDS); + calculateOut(sceneRoot, bounds, mTempLoc); + + final float startX = view.getTranslationX(); + final float endX = startX + mTempLoc[0]; + final float startY = view.getTranslationY(); + final float endY = startY + mTempLoc[1]; + + return createAnimation(view, startX, startY, endX, endY, startX, startY, + sAccelerate); + } + + private void calculateOut(View sceneRoot, Rect bounds, int[] outVector) { + sceneRoot.getLocationOnScreen(mTempLoc); + int sceneRootX = mTempLoc[0]; + int sceneRootY = mTempLoc[1]; + int focalX; + int focalY; + + Rect epicenter = getEpicenter(); + if (epicenter == null) { + focalX = sceneRootX + (sceneRoot.getWidth() / 2) + + Math.round(sceneRoot.getTranslationX()); + focalY = sceneRootY + (sceneRoot.getHeight() / 2) + + Math.round(sceneRoot.getTranslationY()); + } else { + focalX = epicenter.centerX(); + focalY = epicenter.centerY(); + } + + int centerX = bounds.centerX(); + int centerY = bounds.centerY(); + float xVector = centerX - focalX; + float yVector = centerY - focalY; + + if (xVector == 0 && yVector == 0) { + // Random direction when View is centered on focal View. + xVector = (float)(Math.random() * 2) - 1; + yVector = (float)(Math.random() * 2) - 1; + } + float vectorSize = calculateDistance(xVector, yVector); + xVector /= vectorSize; + yVector /= vectorSize; + + float maxDistance = + calculateMaxDistance(sceneRoot, focalX - sceneRootX, focalY - sceneRootY); + + outVector[0] = Math.round(maxDistance * xVector); + outVector[1] = Math.round(maxDistance * yVector); + } + + private static float calculateMaxDistance(View sceneRoot, int focalX, int focalY) { + int maxX = Math.max(focalX, sceneRoot.getWidth() - focalX); + int maxY = Math.max(focalY, sceneRoot.getHeight() - focalY); + return calculateDistance(maxX, maxY); + } + + private static float calculateDistance(float x, float y) { + return FloatMath.sqrt((x * x) + (y * y)); + } + + private static class OutAnimatorListener extends AnimatorListenerAdapter { + private final View mView; + private boolean mCanceled = false; + private float mPausedX; + private float mPausedY; + private final float mTerminalX; + private final float mTerminalY; + private final float mEndX; + private final float mEndY; + + public OutAnimatorListener(View view, float terminalX, float terminalY, + float endX, float endY) { + mView = view; + mTerminalX = terminalX; + mTerminalY = terminalY; + mEndX = endX; + mEndY = endY; + } + + @Override + public void onAnimationCancel(Animator animator) { + mView.setTranslationX(mTerminalX); + mView.setTranslationY(mTerminalY); + mCanceled = true; + } + + @Override + public void onAnimationEnd(Animator animator) { + if (!mCanceled) { + mView.setTranslationX(mTerminalX); + mView.setTranslationY(mTerminalY); + } + } + + @Override + public void onAnimationPause(Animator animator) { + mPausedX = mView.getTranslationX(); + mPausedY = mView.getTranslationY(); + mView.setTranslationY(mEndX); + mView.setTranslationY(mEndY); + } + + @Override + public void onAnimationResume(Animator animator) { + mView.setTranslationX(mPausedX); + mView.setTranslationY(mPausedY); + } + } +} diff --git a/core/java/android/transition/Fade.java b/core/java/android/transition/Fade.java index 8edb1ff..08e27d3 100644 --- a/core/java/android/transition/Fade.java +++ b/core/java/android/transition/Fade.java @@ -59,8 +59,6 @@ public class Fade extends Visibility { private static boolean DBG = Transition.DBG && false; private static final String LOG_TAG = "Fade"; - private static final String PROPNAME_SCREEN_X = "android:fade:screenX"; - private static final String PROPNAME_SCREEN_Y = "android:fade:screenY"; /** * Fading mode used in {@link #Fade(int)} to make the transition @@ -98,245 +96,81 @@ public class Fade extends Visibility { /** * Utility method to handle creating and running the Animator. */ - private Animator createAnimation(View view, float startAlpha, float endAlpha, - AnimatorListenerAdapter listener) { + private Animator createAnimation(View view, float startAlpha, float endAlpha) { if (startAlpha == endAlpha) { - // run listener if we're noop'ing the animation, to get the end-state results now - if (listener != null) { - listener.onAnimationEnd(null); - } return null; } - final ObjectAnimator anim = ObjectAnimator.ofFloat(view, "transitionAlpha", startAlpha, - endAlpha); + view.setTransitionAlpha(startAlpha); + final ObjectAnimator anim = ObjectAnimator.ofFloat(view, "transitionAlpha", endAlpha); if (DBG) { Log.d(LOG_TAG, "Created animator " + anim); } - if (listener != null) { - anim.addListener(listener); - anim.addPauseListener(listener); - } + FadeAnimatorListener listener = new FadeAnimatorListener(view, endAlpha); + anim.addListener(listener); + anim.addPauseListener(listener); return anim; } - private void captureValues(TransitionValues transitionValues) { - int[] loc = new int[2]; - transitionValues.view.getLocationOnScreen(loc); - transitionValues.values.put(PROPNAME_SCREEN_X, loc[0]); - transitionValues.values.put(PROPNAME_SCREEN_Y, loc[1]); - } - - @Override - public void captureStartValues(TransitionValues transitionValues) { - super.captureStartValues(transitionValues); - captureValues(transitionValues); - } - @Override - public Animator onAppear(ViewGroup sceneRoot, - TransitionValues startValues, int startVisibility, - TransitionValues endValues, int endVisibility) { + public Animator onAppear(ViewGroup sceneRoot, View view, + TransitionValues startValues, + TransitionValues endValues) { if ((mFadingMode & IN) != IN || endValues == null) { return null; } - final View endView = endValues.view; if (DBG) { View startView = (startValues != null) ? startValues.view : null; Log.d(LOG_TAG, "Fade.onAppear: startView, startVis, endView, endVis = " + - startView + ", " + startVisibility + ", " + endView + ", " + endVisibility); + startView + ", " + view); } - endView.setTransitionAlpha(0); - TransitionListener transitionListener = new TransitionListenerAdapter() { - boolean mCanceled = false; - float mPausedAlpha; - - @Override - public void onTransitionCancel(Transition transition) { - endView.setTransitionAlpha(1); - mCanceled = true; - } - - @Override - public void onTransitionEnd(Transition transition) { - if (!mCanceled) { - endView.setTransitionAlpha(1); - } - } - - @Override - public void onTransitionPause(Transition transition) { - mPausedAlpha = endView.getTransitionAlpha(); - endView.setTransitionAlpha(1); - } - - @Override - public void onTransitionResume(Transition transition) { - endView.setTransitionAlpha(mPausedAlpha); - } - }; - addListener(transitionListener); - return createAnimation(endView, 0, 1, null); + return createAnimation(view, 0, 1); } @Override - public Animator onDisappear(ViewGroup sceneRoot, - TransitionValues startValues, int startVisibility, - TransitionValues endValues, int endVisibility) { + public Animator onDisappear(ViewGroup sceneRoot, final View view, TransitionValues startValues, + TransitionValues endValues) { if ((mFadingMode & OUT) != OUT) { return null; } - View view = null; - View startView = (startValues != null) ? startValues.view : null; - View endView = (endValues != null) ? endValues.view : null; - if (DBG) { - Log.d(LOG_TAG, "Fade.onDisappear: startView, startVis, endView, endVis = " + - startView + ", " + startVisibility + ", " + endView + ", " + endVisibility); - } - View overlayView = null; - View viewToKeep = null; - if (endView == null || endView.getParent() == null) { - if (endView != null) { - // endView was removed from its parent - add it to the overlay - view = overlayView = endView; - } else if (startView != null) { - // endView does not exist. Use startView only under certain - // conditions, because placing a view in an overlay necessitates - // it being removed from its current parent - if (startView.getParent() == null) { - // no parent - safe to use - view = overlayView = startView; - } else if (startView.getParent() instanceof View && - startView.getParent().getParent() == null) { - View startParent = (View) startView.getParent(); - int id = startParent.getId(); - if (id != View.NO_ID && sceneRoot.findViewById(id) != null && mCanRemoveViews) { - // no parent, but its parent is unparented but the parent - // hierarchy has been replaced by a new hierarchy with the same id - // and it is safe to un-parent startView - view = overlayView = startView; - } - } - } - } else { - // visibility change - if (endVisibility == View.INVISIBLE) { - view = endView; - viewToKeep = view; - } else { - // Becoming GONE - if (startView == endView) { - view = endView; - viewToKeep = view; - } else { - view = startView; - overlayView = view; - } - } - } - final int finalVisibility = endVisibility; - // TODO: add automatic facility to Visibility superclass for keeping views around - if (overlayView != null) { - // TODO: Need to do this for general case of adding to overlay - int screenX = (Integer) startValues.values.get(PROPNAME_SCREEN_X); - int screenY = (Integer) startValues.values.get(PROPNAME_SCREEN_Y); - int[] loc = new int[2]; - sceneRoot.getLocationOnScreen(loc); - overlayView.offsetLeftAndRight((screenX - loc[0]) - overlayView.getLeft()); - overlayView.offsetTopAndBottom((screenY - loc[1]) - overlayView.getTop()); - sceneRoot.getOverlay().add(overlayView); - // TODO: add automatic facility to Visibility superclass for keeping views around - final float startAlpha = 1; - float endAlpha = 0; - final View finalView = view; - final View finalOverlayView = overlayView; - final View finalViewToKeep = viewToKeep; - final ViewGroup finalSceneRoot = sceneRoot; - final AnimatorListenerAdapter endListener = new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - finalView.setTransitionAlpha(startAlpha); - // TODO: restore view offset from overlay repositioning - if (finalViewToKeep != null) { - finalViewToKeep.setVisibility(finalVisibility); - } - if (finalOverlayView != null) { - finalSceneRoot.getOverlay().remove(finalOverlayView); - } - } - @Override - public void onAnimationPause(Animator animation) { - if (finalOverlayView != null) { - finalSceneRoot.getOverlay().remove(finalOverlayView); - } - } + return createAnimation(view, 1, 0); + } + + private static class FadeAnimatorListener extends AnimatorListenerAdapter { + private final View mView; + private final float mEndAlpha; + private boolean mCanceled = false; + private float mPausedAlpha; - @Override - public void onAnimationResume(Animator animation) { - if (finalOverlayView != null) { - finalSceneRoot.getOverlay().add(finalOverlayView); - } - } - }; - return createAnimation(view, startAlpha, endAlpha, endListener); + public FadeAnimatorListener(View view, float endAlpha) { + mView = view; + mEndAlpha = endAlpha; } - if (viewToKeep != null) { - // TODO: find a different way to do this, like just changing the view to be - // VISIBLE for the duration of the transition - viewToKeep.setVisibility((View.VISIBLE)); - // TODO: add automatic facility to Visibility superclass for keeping views around - final float startAlpha = 1; - float endAlpha = 0; - final View finalView = view; - final View finalOverlayView = overlayView; - final View finalViewToKeep = viewToKeep; - final ViewGroup finalSceneRoot = sceneRoot; - final AnimatorListenerAdapter endListener = new AnimatorListenerAdapter() { - boolean mCanceled = false; - float mPausedAlpha = -1; - @Override - public void onAnimationPause(Animator animation) { - if (finalViewToKeep != null && !mCanceled) { - finalViewToKeep.setVisibility(finalVisibility); - } - mPausedAlpha = finalView.getTransitionAlpha(); - finalView.setTransitionAlpha(startAlpha); - } + @Override + public void onAnimationCancel(Animator animator) { + mCanceled = true; + if (mPausedAlpha >= 0) { + mView.setTransitionAlpha(mPausedAlpha); + } + } - @Override - public void onAnimationResume(Animator animation) { - if (finalViewToKeep != null && !mCanceled) { - finalViewToKeep.setVisibility(View.VISIBLE); - } - finalView.setTransitionAlpha(mPausedAlpha); - } + @Override + public void onAnimationEnd(Animator animator) { + if (!mCanceled) { + mView.setTransitionAlpha(mEndAlpha); + } + } - @Override - public void onAnimationCancel(Animator animation) { - mCanceled = true; - if (mPausedAlpha >= 0) { - finalView.setTransitionAlpha(mPausedAlpha); - } - } + @Override + public void onAnimationPause(Animator animator) { + mPausedAlpha = mView.getTransitionAlpha(); + mView.setTransitionAlpha(mEndAlpha); + } - @Override - public void onAnimationEnd(Animator animation) { - if (!mCanceled) { - finalView.setTransitionAlpha(startAlpha); - } - // TODO: restore view offset from overlay repositioning - if (finalViewToKeep != null && !mCanceled) { - finalViewToKeep.setVisibility(finalVisibility); - } - if (finalOverlayView != null) { - finalSceneRoot.getOverlay().remove(finalOverlayView); - } - } - }; - return createAnimation(view, startAlpha, endAlpha, endListener); + @Override + public void onAnimationResume(Animator animator) { + mView.setTransitionAlpha(mPausedAlpha); } - return null; } - -}
\ No newline at end of file +} diff --git a/core/java/android/transition/MatrixClippedDrawable.java b/core/java/android/transition/MatrixClippedDrawable.java new file mode 100644 index 0000000..ebaad59 --- /dev/null +++ b/core/java/android/transition/MatrixClippedDrawable.java @@ -0,0 +1,300 @@ +/* + * Copyright (C) 2014 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.transition; + +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.Property; + +/** + * Used in MoveImage to mock an ImageView as a Drawable to be scaled in the scene root Overlay. + * @hide + */ +class MatrixClippedDrawable extends Drawable implements Drawable.Callback { + private static final String TAG = "MatrixClippedDrawable"; + + private ClippedMatrixState mClippedMatrixState; + + public static final Property<MatrixClippedDrawable, Rect> CLIP_PROPERTY + = new Property<MatrixClippedDrawable, Rect>(Rect.class, "clipRect") { + + @Override + public Rect get(MatrixClippedDrawable object) { + return object.getClipRect(); + } + + @Override + public void set(MatrixClippedDrawable object, Rect value) { + object.setClipRect(value); + } + }; + + public static final Property<MatrixClippedDrawable, Matrix> MATRIX_PROPERTY + = new Property<MatrixClippedDrawable, Matrix>(Matrix.class, "matrix") { + @Override + public void set(MatrixClippedDrawable object, Matrix value) { + object.setMatrix(value); + } + + @Override + public Matrix get(MatrixClippedDrawable object) { + return object.getMatrix(); + } + }; + + public MatrixClippedDrawable(Drawable drawable) { + this(null, null); + + mClippedMatrixState.mDrawable = drawable; + + if (drawable != null) { + drawable.setCallback(this); + } + } + + public void setMatrix(Matrix matrix) { + if (matrix == null) { + mClippedMatrixState.mMatrix = null; + } else { + if (mClippedMatrixState.mMatrix == null) { + mClippedMatrixState.mMatrix = new Matrix(); + } + mClippedMatrixState.mMatrix.set(matrix); + } + invalidateSelf(); + } + + public Matrix getMatrix() { + return mClippedMatrixState.mMatrix; + } + + public Rect getClipRect() { + return mClippedMatrixState.mClipRect; + } + + public void setClipRect(Rect clipRect) { + if (clipRect == null) { + if (mClippedMatrixState.mClipRect != null) { + mClippedMatrixState.mClipRect = null; + invalidateSelf(); + } + } else { + if (mClippedMatrixState.mClipRect == null) { + mClippedMatrixState.mClipRect = new Rect(clipRect); + } else { + mClippedMatrixState.mClipRect.set(clipRect); + } + invalidateSelf(); + } + } + + // overrides from Drawable.Callback + + public void invalidateDrawable(Drawable who) { + final Drawable.Callback callback = getCallback(); + if (callback != null) { + callback.invalidateDrawable(this); + } + } + + public void scheduleDrawable(Drawable who, Runnable what, long when) { + final Drawable.Callback callback = getCallback(); + if (callback != null) { + callback.scheduleDrawable(this, what, when); + } + } + + public void unscheduleDrawable(Drawable who, Runnable what) { + final Drawable.Callback callback = getCallback(); + if (callback != null) { + callback.unscheduleDrawable(this, what); + } + } + + // overrides from Drawable + + @Override + public int getChangingConfigurations() { + return super.getChangingConfigurations() + | mClippedMatrixState.mChangingConfigurations + | mClippedMatrixState.mDrawable.getChangingConfigurations(); + } + + @Override + public boolean getPadding(Rect padding) { + // XXX need to adjust padding! + return mClippedMatrixState.mDrawable.getPadding(padding); + } + + @Override + public boolean setVisible(boolean visible, boolean restart) { + mClippedMatrixState.mDrawable.setVisible(visible, restart); + return super.setVisible(visible, restart); + } + + @Override + public void setAlpha(int alpha) { + mClippedMatrixState.mDrawable.setAlpha(alpha); + } + + @Override + public int getAlpha() { + return mClippedMatrixState.mDrawable.getAlpha(); + } + + @Override + public void setColorFilter(ColorFilter cf) { + mClippedMatrixState.mDrawable.setColorFilter(cf); + } + + @Override + public int getOpacity() { + return mClippedMatrixState.mDrawable.getOpacity(); + } + + @Override + public boolean isStateful() { + return mClippedMatrixState.mDrawable.isStateful(); + } + + @Override + protected boolean onStateChange(int[] state) { + return mClippedMatrixState.mDrawable.setState(state); + } + + @Override + protected boolean onLevelChange(int level) { + mClippedMatrixState.mDrawable.setLevel(level); + invalidateSelf(); + return true; + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.setBounds(bounds); + if (mClippedMatrixState.mMatrix == null) { + mClippedMatrixState.mDrawable.setBounds(bounds); + } else { + int drawableWidth = mClippedMatrixState.mDrawable.getIntrinsicWidth(); + int drawableHeight = mClippedMatrixState.mDrawable.getIntrinsicHeight(); + mClippedMatrixState.mDrawable.setBounds(bounds.left, bounds.top, + drawableWidth + bounds.left, drawableHeight + bounds.top); + } + invalidateSelf(); + } + + @Override + public void draw(Canvas canvas) { + Rect bounds = getBounds(); + int left = bounds.left; + int top = bounds.top; + int saveCount = canvas.getSaveCount(); + canvas.save(); + if (mClippedMatrixState.mClipRect != null) { + canvas.clipRect(mClippedMatrixState.mClipRect); + } else { + canvas.clipRect(bounds); + } + + if (mClippedMatrixState != null && !mClippedMatrixState.mMatrix.isIdentity()) { + canvas.translate(left, top); + canvas.concat(mClippedMatrixState.mMatrix); + canvas.translate(-left, -top); + } + mClippedMatrixState.mDrawable.draw(canvas); + canvas.restoreToCount(saveCount); + } + + @Override + public int getIntrinsicWidth() { + return mClippedMatrixState.mDrawable.getIntrinsicWidth(); + } + + @Override + public int getIntrinsicHeight() { + return mClippedMatrixState.mDrawable.getIntrinsicHeight(); + } + + @Override + public Drawable.ConstantState getConstantState() { + if (mClippedMatrixState.canConstantState()) { + mClippedMatrixState.mChangingConfigurations = getChangingConfigurations(); + return mClippedMatrixState; + } + return null; + } + + final static class ClippedMatrixState extends Drawable.ConstantState { + Drawable mDrawable; + Matrix mMatrix; + Rect mClipRect; + + private boolean mCheckedConstantState; + private boolean mCanConstantState; + int mChangingConfigurations; + + ClippedMatrixState(ClippedMatrixState orig, MatrixClippedDrawable owner, Resources res) { + if (orig != null) { + if (res != null) { + mDrawable = orig.mDrawable.getConstantState().newDrawable(res); + } else { + mDrawable = orig.mDrawable.getConstantState().newDrawable(); + } + mDrawable.setCallback(owner); + mCheckedConstantState = mCanConstantState = true; + if (orig.mMatrix != null) { + mMatrix = new Matrix(orig.mMatrix); + } + if (orig.mClipRect != null) { + mClipRect = new Rect(orig.mClipRect); + } + } + } + + @Override + public Drawable newDrawable() { + return new MatrixClippedDrawable(this, null); + } + + @Override + public Drawable newDrawable(Resources res) { + return new MatrixClippedDrawable(this, res); + } + + @Override + public int getChangingConfigurations() { + return mChangingConfigurations; + } + + boolean canConstantState() { + if (!mCheckedConstantState) { + mCanConstantState = mDrawable.getConstantState() != null; + mCheckedConstantState = true; + } + + return mCanConstantState; + } + } + + private MatrixClippedDrawable(ClippedMatrixState state, Resources res) { + mClippedMatrixState = new ClippedMatrixState(state, this, res); + } + +} diff --git a/core/java/android/transition/MoveImage.java b/core/java/android/transition/MoveImage.java new file mode 100644 index 0000000..d68e971 --- /dev/null +++ b/core/java/android/transition/MoveImage.java @@ -0,0 +1,326 @@ +/* + * Copyright (C) 2014 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.transition; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.animation.RectEvaluator; +import android.animation.TypeEvaluator; +import android.animation.ValueAnimator; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.util.FloatMath; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroupOverlay; +import android.view.ViewParent; +import android.widget.ImageView; + +import java.util.ArrayList; +import java.util.Map; + +/** + * Transitions ImageViews, including size, scaleType, and matrix. The ImageView drawable + * must remain the same between both start and end states, but the + * {@link ImageView#setScaleType(android.widget.ImageView.ScaleType)} may + * differ. + */ +public class MoveImage extends Transition { + private static final String TAG = "MoveImage"; + private static final String PROPNAME_MATRIX = "android:moveImage:matrix"; + private static final String PROPNAME_BOUNDS = "android:moveImage:bounds"; + private static final String PROPNAME_CLIP = "android:moveImage:clip"; + private static final String PROPNAME_DRAWABLE = "android:moveImage:drawable"; + + private int[] mTempLoc = new int[2]; + + private static final String[] sTransitionProperties = { + PROPNAME_MATRIX, + PROPNAME_BOUNDS, + PROPNAME_CLIP, + PROPNAME_DRAWABLE, + }; + + private void captureValues(TransitionValues transitionValues) { + View view = transitionValues.view; + if (!(view instanceof ImageView) || view.getVisibility() != View.VISIBLE) { + return; + } + Map<String, Object> values = transitionValues.values; + + ViewGroup parent = (ViewGroup) view.getParent(); + parent.getLocationInWindow(mTempLoc); + int paddingLeft = view.getPaddingLeft(); + int paddingTop = view.getPaddingTop(); + int paddingRight = view.getPaddingRight(); + int paddingBottom = view.getPaddingBottom(); + int left = mTempLoc[0] + paddingLeft + view.getLeft() + Math.round(view.getTranslationX()); + int top = mTempLoc[1] + paddingTop + view.getTop() + Math.round(view.getTranslationY()); + int right = left + view.getWidth() - paddingRight - paddingLeft; + int bottom = top + view.getHeight() - paddingTop - paddingBottom; + + Rect bounds = new Rect(left, top, right, bottom); + values.put(PROPNAME_BOUNDS, bounds); + ImageView imageView = (ImageView) view; + Matrix matrix = getMatrix(imageView); + values.put(PROPNAME_MATRIX, matrix); + values.put(PROPNAME_CLIP, findClip(imageView)); + values.put(PROPNAME_DRAWABLE, imageView.getDrawable()); + } + + @Override + public void captureStartValues(TransitionValues transitionValues) { + captureValues(transitionValues); + } + + @Override + public void captureEndValues(TransitionValues transitionValues) { + captureValues(transitionValues); + } + + @Override + public String[] getTransitionProperties() { + return sTransitionProperties; + } + + /** + * Creates an Animator for ImageViews moving, changing dimensions, and/or changing + * {@link android.widget.ImageView.ScaleType}. + * @param sceneRoot The root of the transition hierarchy. + * @param startValues The values for a specific target in the start scene. + * @param endValues The values for the target in the end scene. + * @return An Animator to move an ImageView or null if the View is not an ImageView, + * the Drawable changed, the View is not VISIBLE, or there was no change. + */ + @Override + public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, + TransitionValues endValues) { + if (startValues == null || endValues == null + || startValues.values.get(PROPNAME_BOUNDS) == null + || endValues.values.get(PROPNAME_BOUNDS) == null + || startValues.values.get(PROPNAME_DRAWABLE) + != endValues.values.get(PROPNAME_DRAWABLE)) { + return null; + } + ArrayList<PropertyValuesHolder> changes = new ArrayList<PropertyValuesHolder>(); + + Matrix startMatrix = (Matrix) startValues.values.get(PROPNAME_MATRIX); + Matrix endMatrix = (Matrix) endValues.values.get(PROPNAME_MATRIX); + + if (!startMatrix.equals(endMatrix)) { + changes.add(PropertyValuesHolder.ofObject(MatrixClippedDrawable.MATRIX_PROPERTY, + new MatrixEvaluator(), startMatrix, endMatrix)); + } + + sceneRoot.getLocationInWindow(mTempLoc); + int rootX = mTempLoc[0]; + int rootY = mTempLoc[1]; + final ImageView imageView = (ImageView) endValues.view; + + Drawable drawable = imageView.getDrawable(); + + Rect startBounds = new Rect((Rect) startValues.values.get(PROPNAME_BOUNDS)); + Rect endBounds = new Rect((Rect) endValues.values.get(PROPNAME_BOUNDS)); + startBounds.offset(-rootX, -rootY); + endBounds.offset(-rootX, -rootY); + + if (!startBounds.equals(endBounds)) { + changes.add(PropertyValuesHolder.ofObject("bounds", new RectEvaluator(new Rect()), + startBounds, endBounds)); + } + + Rect startClip = (Rect) startValues.values.get(PROPNAME_CLIP); + Rect endClip = (Rect) endValues.values.get(PROPNAME_CLIP); + if (startClip != null || endClip != null) { + startClip = nonNullClip(startClip, sceneRoot, rootX, rootY); + endClip = nonNullClip(endClip, sceneRoot, rootX, rootY); + + expandClip(startBounds, startMatrix, startClip, endClip); + expandClip(endBounds, endMatrix, endClip, startClip); + boolean clipped = !startClip.contains(startBounds) || !endClip.contains(endBounds); + if (!clipped) { + startClip = null; + } else if (!startClip.equals(endClip)) { + changes.add(PropertyValuesHolder.ofObject(MatrixClippedDrawable.CLIP_PROPERTY, + new RectEvaluator(), startClip, endClip)); + } + } + + if (changes.isEmpty()) { + return null; + } + + drawable = drawable.getConstantState().newDrawable(); + final MatrixClippedDrawable matrixClippedDrawable = new MatrixClippedDrawable(drawable); + matrixClippedDrawable.setMatrix(startMatrix); + matrixClippedDrawable.setBounds(startBounds); + matrixClippedDrawable.setClipRect(startClip); + + imageView.setVisibility(View.INVISIBLE); + final ViewGroupOverlay overlay = sceneRoot.getOverlay(); + overlay.add(matrixClippedDrawable); + ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(matrixClippedDrawable, + changes.toArray(new PropertyValuesHolder[changes.size()])); + + AnimatorListenerAdapter listener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + imageView.setVisibility(View.VISIBLE); + overlay.remove(matrixClippedDrawable); + } + + @Override + public void onAnimationPause(Animator animation) { + imageView.setVisibility(View.VISIBLE); + overlay.remove(matrixClippedDrawable); + } + + @Override + public void onAnimationResume(Animator animation) { + imageView.setVisibility(View.INVISIBLE); + overlay.add(matrixClippedDrawable); + } + }; + + animator.addListener(listener); + animator.addPauseListener(listener); + + return animator; + } + + private static Rect nonNullClip(Rect clip, ViewGroup sceneRoot, int rootX, int rootY) { + if (clip != null) { + clip = new Rect(clip); + clip.offset(-rootX, -rootY); + } else { + clip = new Rect(0, 0, sceneRoot.getWidth(), sceneRoot.getHeight()); + } + return clip; + } + + private static void expandClip(Rect bounds, Matrix matrix, Rect clip, Rect otherClip) { + RectF boundsF = new RectF(bounds); + matrix.mapRect(boundsF); + clip.left = expandMinDimension(boundsF.left, clip.left, otherClip.left); + clip.top = expandMinDimension(boundsF.top, clip.top, otherClip.top); + clip.right = expandMaxDimension(boundsF.right, clip.right, otherClip.right); + clip.bottom = expandMaxDimension(boundsF.bottom, clip.bottom, otherClip.bottom); + } + + private static int expandMinDimension(float boundsDimension, int clipDimension, + int otherClipDimension) { + if (clipDimension > boundsDimension) { + // Already clipped in that dimension, return the clipped value + return clipDimension; + } + return Math.min(clipDimension, otherClipDimension); + } + + private static int expandMaxDimension(float boundsDimension, int clipDimension, + int otherClipDimension) { + return -expandMinDimension(-boundsDimension, -clipDimension, -otherClipDimension); + } + + private static Matrix getMatrix(ImageView imageView) { + Drawable drawable = imageView.getDrawable(); + int drawableWidth = drawable.getIntrinsicWidth(); + int drawableHeight = drawable.getIntrinsicHeight(); + ImageView.ScaleType scaleType = imageView.getScaleType(); + if (drawableWidth <= 0 || drawableHeight <= 0 || scaleType == ImageView.ScaleType.FIT_XY) { + return null; + } + return new Matrix(imageView.getImageMatrix()); + } + + private Rect findClip(ImageView imageView) { + if (imageView.getCropToPadding()) { + Rect clip = getClip(imageView); + clip.left += imageView.getPaddingLeft(); + clip.right -= imageView.getPaddingRight(); + clip.top += imageView.getPaddingTop(); + clip.bottom -= imageView.getPaddingBottom(); + return clip; + } else { + View view = imageView; + ViewParent viewParent; + while ((viewParent = view.getParent()) instanceof ViewGroup) { + ViewGroup viewGroup = (ViewGroup) viewParent; + if (viewGroup.getClipChildren()) { + Rect clip = getClip(view); + return clip; + } + view = viewGroup; + } + } + return null; + } + + private Rect getClip(View clipView) { + Rect clipBounds = clipView.getClipBounds(); + if (clipBounds == null) { + clipBounds = new Rect(clipView.getLeft(), clipView.getTop(), + clipView.getRight(), clipView.getBottom()); + } + + ViewParent parent = clipView.getParent(); + if (parent instanceof ViewGroup) { + ViewGroup parentViewGroup = (ViewGroup) parent; + parentViewGroup.getLocationInWindow(mTempLoc); + clipBounds.offset(mTempLoc[0], mTempLoc[1]); + } + + return clipBounds; + } + + @Override + public Transition clone() { + MoveImage clone = (MoveImage) super.clone(); + clone.mTempLoc = new int[2]; + return clone; + } + + private static class MatrixEvaluator implements TypeEvaluator<Matrix> { + static final Matrix sIdentity = new Matrix(); + float[] mTempStartValues = new float[9]; + float[] mTempEndValues = new float[9]; + Matrix mTempMatrix = new Matrix(); + + @Override + public Matrix evaluate(float fraction, Matrix startValue, Matrix endValue) { + if (startValue == null && endValue == null) { + return null; + } + if (startValue == null) { + startValue = sIdentity; + } else if (endValue == null) { + endValue = sIdentity; + } + startValue.getValues(mTempStartValues); + endValue.getValues(mTempEndValues); + for (int i = 0; i < 9; i++) { + float diff = mTempEndValues[i] - mTempStartValues[i]; + mTempEndValues[i] = mTempStartValues[i] + (fraction * diff); + } + mTempMatrix.setValues(mTempEndValues); + return mTempMatrix; + } + } +} diff --git a/core/java/android/transition/Recolor.java b/core/java/android/transition/Recolor.java index 70111d1..1638f67 100644 --- a/core/java/android/transition/Recolor.java +++ b/core/java/android/transition/Recolor.java @@ -17,7 +17,6 @@ package android.transition; import android.animation.Animator; -import android.animation.ArgbEvaluator; import android.animation.ObjectAnimator; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; @@ -75,8 +74,8 @@ public class Recolor extends Transition { if (startColor.getColor() != endColor.getColor()) { endColor.setColor(startColor.getColor()); changed = true; - return ObjectAnimator.ofObject(endBackground, "color", - new ArgbEvaluator(), startColor.getColor(), endColor.getColor()); + return ObjectAnimator.ofArgb(endBackground, "color", startColor.getColor(), + endColor.getColor()); } } if (view instanceof TextView) { @@ -86,8 +85,7 @@ public class Recolor extends Transition { if (start != end) { textView.setTextColor(end); changed = true; - return ObjectAnimator.ofObject(textView, "textColor", - new ArgbEvaluator(), start, end); + return ObjectAnimator.ofArgb(textView, "textColor", start, end); } } return null; diff --git a/core/java/android/transition/Scene.java b/core/java/android/transition/Scene.java index e1f1896..4267a65 100644 --- a/core/java/android/transition/Scene.java +++ b/core/java/android/transition/Scene.java @@ -34,7 +34,7 @@ public final class Scene { private Context mContext; private int mLayoutId = -1; private ViewGroup mSceneRoot; - private ViewGroup mLayout; // alternative to layoutId + private View mLayout; // alternative to layoutId Runnable mEnterAction, mExitAction; /** @@ -114,6 +114,15 @@ public final class Scene { * @param layout The view hierarchy of this scene, added as a child * of sceneRoot when this scene is entered. */ + public Scene(ViewGroup sceneRoot, View layout) { + mSceneRoot = sceneRoot; + mLayout = layout; + } + + /** + * @deprecated use {@link #Scene(ViewGroup, View)}. + */ + @Deprecated public Scene(ViewGroup sceneRoot, ViewGroup layout) { mSceneRoot = sceneRoot; mLayout = layout; diff --git a/core/java/android/transition/SidePropagation.java b/core/java/android/transition/SidePropagation.java new file mode 100644 index 0000000..c331945 --- /dev/null +++ b/core/java/android/transition/SidePropagation.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2014 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.transition; + +import android.graphics.Rect; +import android.util.FloatMath; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +/** + * A <code>TransitionPropagation</code> that propagates based on the distance to the side + * and, orthogonally, the distance to epicenter. If the transitioning View is visible in + * the start of the transition, then it will transition sooner when closer to the side and + * later when farther. If the view is not visible in the start of the transition, then + * it will transition later when closer to the side and sooner when farther from the edge. + * This is the default TransitionPropagation used with {@link android.transition.Slide}. + */ +public class SidePropagation extends VisibilityPropagation { + private static final String TAG = "SlidePropagation"; + + /** + * Transition propagates relative to the distance of the left side of the scene. + */ + public static final int LEFT = Slide.LEFT; + + /** + * Transition propagates relative to the distance of the top of the scene. + */ + public static final int TOP = Slide.TOP; + + /** + * Transition propagates relative to the distance of the right side of the scene. + */ + public static final int RIGHT = Slide.RIGHT; + + /** + * Transition propagates relative to the distance of the bottom of the scene. + */ + public static final int BOTTOM = Slide.BOTTOM; + + private float mPropagationSpeed = 4.0f; + private int mSide = BOTTOM; + + /** + * Sets the side that is used to calculate the transition propagation. If the transitioning + * View is visible in the start of the transition, then it will transition sooner when + * closer to the side and later when farther. If the view is not visible in the start of + * the transition, then it will transition later when closer to the side and sooner when + * farther from the edge. The default is {@link #BOTTOM}. + * + * @param side The side that is used to calculate the transition propagation. Must be one of + * {@link #LEFT}, {@link #TOP}, {@link #RIGHT}, or {@link #BOTTOM}. + */ + public void setSide(int side) { + mSide = side; + } + + /** + * Sets the speed at which transition propagation happens, relative to the duration of the + * Transition. A <code>propagationSpeed</code> of 1 means that a View centered at the side + * set in {@link #setSide(int)} and View centered at the opposite edge will have a difference + * in start delay of approximately the duration of the Transition. A speed of 2 means the + * start delay difference will be approximately half of the duration of the transition. A + * value of 0 is illegal, but negative values will invert the propagation. + * + * @param propagationSpeed The speed at which propagation occurs, relative to the duration + * of the transition. A speed of 4 means it works 4 times as fast + * as the duration of the transition. May not be 0. + */ + public void setPropagationSpeed(float propagationSpeed) { + if (propagationSpeed == 0) { + throw new IllegalArgumentException("propagationSpeed may not be 0"); + } + mPropagationSpeed = propagationSpeed; + } + + @Override + public long getStartDelay(ViewGroup sceneRoot, Transition transition, + TransitionValues startValues, TransitionValues endValues) { + if (startValues == null && endValues == null) { + return 0; + } + int directionMultiplier = 1; + Rect epicenter = transition.getEpicenter(); + TransitionValues positionValues; + if (endValues == null || getViewVisibility(startValues) == View.VISIBLE) { + positionValues = startValues; + directionMultiplier = -1; + } else { + positionValues = endValues; + } + + int viewCenterX = getViewX(positionValues); + int viewCenterY = getViewY(positionValues); + + int[] loc = new int[2]; + sceneRoot.getLocationOnScreen(loc); + int left = loc[0] + Math.round(sceneRoot.getTranslationX()); + int top = loc[1] + Math.round(sceneRoot.getTranslationY()); + int right = left + sceneRoot.getWidth(); + int bottom = top + sceneRoot.getHeight(); + + int epicenterX; + int epicenterY; + if (epicenter != null) { + epicenterX = epicenter.centerX(); + epicenterY = epicenter.centerY(); + } else { + epicenterX = (left + right) / 2; + epicenterY = (top + bottom) / 2; + } + + float distance = distance(viewCenterX, viewCenterY, epicenterX, epicenterY, + left, top, right, bottom); + float maxDistance = getMaxDistance(sceneRoot); + float distanceFraction = distance/maxDistance; + + return Math.round(transition.getDuration() * directionMultiplier / mPropagationSpeed + * distanceFraction); + } + + private int distance(int viewX, int viewY, int epicenterX, int epicenterY, + int left, int top, int right, int bottom) { + int distance = 0; + switch (mSide) { + case LEFT: + distance = right - viewX + Math.abs(epicenterY - viewY); + break; + case TOP: + distance = bottom - viewY + Math.abs(epicenterX - viewX); + break; + case RIGHT: + distance = viewX - left + Math.abs(epicenterY - viewY); + break; + case BOTTOM: + distance = viewY - top + Math.abs(epicenterX - viewX); + break; + } + return distance; + } + + private int getMaxDistance(ViewGroup sceneRoot) { + switch (mSide) { + case LEFT: + case RIGHT: + return sceneRoot.getWidth(); + default: + return sceneRoot.getHeight(); + } + } +} diff --git a/core/java/android/transition/Slide.java b/core/java/android/transition/Slide.java index b38973c..0ff8ddd 100644 --- a/core/java/android/transition/Slide.java +++ b/core/java/android/transition/Slide.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 The Android Open Source Project + * Copyright (C) 2014 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. @@ -13,53 +13,240 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package android.transition; import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.graphics.Rect; +import android.util.Log; +import android.util.Property; import android.view.View; import android.view.ViewGroup; import android.view.animation.AccelerateInterpolator; import android.view.animation.DecelerateInterpolator; /** - * This transition captures the visibility of target objects before and - * after a scene change and animates any changes by sliding the target - * objects into or out of place. - * - * @hide + * This transition tracks changes to the visibility of target views in the + * start and end scenes and moves views in or out from one of the edges of the + * scene. Visibility is determined by both the + * {@link View#setVisibility(int)} state of the view as well as whether it + * is parented in the current view hierarchy. Disappearing Views are + * limited as described in {@link Visibility#onDisappear(android.view.ViewGroup, + * TransitionValues, int, TransitionValues, int)}. */ public class Slide extends Visibility { + private static final String TAG = "Slide"; - // TODO: Add parameter for sliding factor - it's hard-coded below + /** + * Move Views in or out of the left edge of the scene. + * @see #setSlideEdge(int) + */ + public static final int LEFT = 0; - private static final TimeInterpolator sAccelerator = new AccelerateInterpolator(); - private static final TimeInterpolator sDecelerator = new DecelerateInterpolator(); + /** + * Move Views in or out of the top edge of the scene. + * @see #setSlideEdge(int) + */ + public static final int TOP = 1; - @Override - public Animator onAppear(ViewGroup sceneRoot, - TransitionValues startValues, int startVisibility, - TransitionValues endValues, int endVisibility) { - View endView = (endValues != null) ? endValues.view : null; - endView.setTranslationY(-2 * endView.getHeight()); - ObjectAnimator anim = ObjectAnimator.ofFloat(endView, View.TRANSLATION_Y, - -2 * endView.getHeight(), 0); - anim.setInterpolator(sDecelerator); + /** + * Move Views in or out of the right edge of the scene. + * @see #setSlideEdge(int) + */ + public static final int RIGHT = 2; + + /** + * Move Views in or out of the bottom edge of the scene. This is the + * default slide direction. + * @see #setSlideEdge(int) + */ + public static final int BOTTOM = 3; + + private static final TimeInterpolator sDecelerate = new DecelerateInterpolator(); + private static final TimeInterpolator sAccelerate = new AccelerateInterpolator(); + + private int[] mTempLoc = new int[2]; + private CalculateSlide mSlideCalculator = sCalculateBottom; + + private interface CalculateSlide { + /** Returns the translation value for view when it out of the scene */ + float getGone(ViewGroup sceneRoot, View view); + + /** Returns the translation value for view when it is in the scene */ + float getHere(View view); + + /** Returns the property to animate translation */ + Property<View, Float> getProperty(); + } + + private static abstract class CalculateSlideHorizontal implements CalculateSlide { + @Override + public float getHere(View view) { + return view.getTranslationX(); + } + + @Override + public Property<View, Float> getProperty() { + return View.TRANSLATION_X; + } + } + + private static abstract class CalculateSlideVertical implements CalculateSlide { + @Override + public float getHere(View view) { + return view.getTranslationY(); + } + + @Override + public Property<View, Float> getProperty() { + return View.TRANSLATION_Y; + } + } + + private static final CalculateSlide sCalculateLeft = new CalculateSlideHorizontal() { + @Override + public float getGone(ViewGroup sceneRoot, View view) { + return view.getTranslationX() - sceneRoot.getWidth(); + } + }; + + private static final CalculateSlide sCalculateTop = new CalculateSlideVertical() { + @Override + public float getGone(ViewGroup sceneRoot, View view) { + return view.getTranslationY() - sceneRoot.getHeight(); + } + }; + + private static final CalculateSlide sCalculateRight = new CalculateSlideHorizontal() { + @Override + public float getGone(ViewGroup sceneRoot, View view) { + return view.getTranslationX() + sceneRoot.getWidth(); + } + }; + + private static final CalculateSlide sCalculateBottom = new CalculateSlideVertical() { + @Override + public float getGone(ViewGroup sceneRoot, View view) { + return view.getTranslationY() + sceneRoot.getHeight(); + } + }; + + /** + * Constructor using the default {@link android.transition.Slide#BOTTOM} + * slide edge direction. + */ + public Slide() { + setSlideEdge(BOTTOM); + } + + /** + * Constructor using the provided slide edge direction. + */ + public Slide(int slideEdge) { + setSlideEdge(slideEdge); + } + + /** + * Change the edge that Views appear and disappear from. + * @param slideEdge The edge of the scene to use for Views appearing and disappearing. + */ + public void setSlideEdge(int slideEdge) { + switch (slideEdge) { + case LEFT: + mSlideCalculator = sCalculateLeft; + break; + case TOP: + mSlideCalculator = sCalculateTop; + break; + case RIGHT: + mSlideCalculator = sCalculateRight; + break; + case BOTTOM: + mSlideCalculator = sCalculateBottom; + break; + default: + throw new IllegalArgumentException("Invalid slide direction"); + } + SidePropagation propagation = new SidePropagation(); + propagation.setSide(slideEdge); + setPropagation(propagation); + } + + private Animator createAnimation(final View view, Property<View, Float> property, + float start, float end, float terminalValue, TimeInterpolator interpolator) { + view.setTranslationY(start); + if (start == end) { + return null; + } + final ObjectAnimator anim = ObjectAnimator.ofFloat(view, property, start, end); + + SlideAnimatorListener listener = new SlideAnimatorListener(view, terminalValue, end); + anim.addListener(listener); + anim.addPauseListener(listener); + anim.setInterpolator(interpolator); return anim; } @Override - public Animator onDisappear(ViewGroup sceneRoot, - TransitionValues startValues, int startVisibility, - TransitionValues endValues, int endVisibility) { - View startView = (startValues != null) ? startValues.view : null; - startView.setTranslationY(0); - ObjectAnimator anim = ObjectAnimator.ofFloat(startView, View.TRANSLATION_Y, 0, - -2 * startView.getHeight()); - anim.setInterpolator(sAccelerator); - return anim; + public Animator onAppear(ViewGroup sceneRoot, View view, + TransitionValues startValues, TransitionValues endValues) { + if (endValues == null) { + return null; + } + float end = mSlideCalculator.getHere(view); + float start = mSlideCalculator.getGone(sceneRoot, view); + return createAnimation(view, mSlideCalculator.getProperty(), start, end, end, sDecelerate); } + @Override + public Animator onDisappear(ViewGroup sceneRoot, View view, + TransitionValues startValues, TransitionValues endValues) { + float start = mSlideCalculator.getHere(view); + float end = mSlideCalculator.getGone(sceneRoot, view); + + return createAnimation(view, mSlideCalculator.getProperty(), start, end, start, + sAccelerate); + } + + private static class SlideAnimatorListener extends AnimatorListenerAdapter { + private boolean mCanceled = false; + private float mPausedY; + private final View mView; + private final float mEndY; + private final float mTerminalY; + + public SlideAnimatorListener(View view, float terminalY, float endY) { + mView = view; + mTerminalY = terminalY; + mEndY = endY; + } + + @Override + public void onAnimationCancel(Animator animator) { + mView.setTranslationY(mTerminalY); + mCanceled = true; + } + + @Override + public void onAnimationEnd(Animator animator) { + if (!mCanceled) { + mView.setTranslationY(mTerminalY); + } + } + + @Override + public void onAnimationPause(Animator animator) { + mPausedY = mView.getTranslationY(); + mView.setTranslationY(mEndY); + } + + @Override + public void onAnimationResume(Animator animator) { + mView.setTranslationY(mPausedY); + } + } } diff --git a/core/java/android/transition/Transition.java b/core/java/android/transition/Transition.java index da9ba5a..b7ae31e 100644 --- a/core/java/android/transition/Transition.java +++ b/core/java/android/transition/Transition.java @@ -19,15 +19,18 @@ package android.transition; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.TimeInterpolator; +import android.graphics.Rect; import android.util.ArrayMap; import android.util.Log; import android.util.LongSparseArray; import android.util.SparseArray; +import android.util.SparseLongArray; import android.view.SurfaceView; import android.view.TextureView; import android.view.View; import android.view.ViewGroup; import android.view.ViewOverlay; +import android.view.WindowId; import android.widget.ListView; import android.widget.Spinner; @@ -59,10 +62,18 @@ import java.util.List; * <p>Transitions can be declared in XML resource files inside the <code>res/transition</code> * directory. Transition resources consist of a tag name for one of the Transition * subclasses along with attributes to define some of the attributes of that transition. - * For example, here is a minimal resource file that declares a {@link ChangeBounds} transition:</p> + * For example, here is a minimal resource file that declares a {@link ChangeBounds} transition: * * {@sample development/samples/ApiDemos/res/transition/changebounds.xml ChangeBounds} * + * <p>{@link android.transition.Explode} transition:</p> + * + * {@sample development/samples/ApiDemos/res/transition/explode.xml Explode} + * + * <p>{@link android.transition.MoveImage} transition:</p> + * + * {@sample development/samples/ApiDemos/res/transition/move_image.xml MoveImage} + * * <p>Note that attributes for the transition are not required, just as they are * optional when declared in code; Transitions created from XML resources will use * the same defaults as their code-created equivalents. Here is a slightly more @@ -86,7 +97,8 @@ import java.util.List; * * Further information on XML resource descriptions for transitions can be found for * {@link android.R.styleable#Transition}, {@link android.R.styleable#TransitionSet}, - * {@link android.R.styleable#TransitionTarget}, and {@link android.R.styleable#Fade}. + * {@link android.R.styleable#TransitionTarget}, {@link android.R.styleable#Fade}, and + * {@link android.R.styleable#Slide}. * */ public abstract class Transition implements Cloneable { @@ -148,6 +160,13 @@ public abstract class Transition implements Cloneable { // to be run in runAnimators() ArrayList<Animator> mAnimators = new ArrayList<Animator>(); + // The function for calculating the Animation start delay. + TransitionPropagation mPropagation; + + // The rectangular region for Transitions like Explode and TransitionPropagations + // like CircularPropagation + EpicenterCallback mEpicenterCallback; + /** * Constructs a Transition object with no target objects. A transition with * no targets defaults to running on all target objects in the scene hierarchy @@ -434,6 +453,9 @@ public abstract class Transition implements Cloneable { endValuesList.add(end); } ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators(); + long minStartDelay = Long.MAX_VALUE; + int minAnimator = mAnimators.size(); + SparseLongArray startDelays = new SparseLongArray(); for (int i = 0; i < startValuesList.size(); ++i) { TransitionValues start = startValuesList.get(i); TransitionValues end = endValuesList.get(i); @@ -496,7 +518,14 @@ public abstract class Transition implements Cloneable { view = (start != null) ? start.view : null; } if (animator != null) { - AnimationInfo info = new AnimationInfo(view, getName(), infoValues); + if (mPropagation != null) { + long delay = mPropagation + .getStartDelay(sceneRoot, this, start, end); + startDelays.put(mAnimators.size(), delay); + minStartDelay = Math.min(delay, minStartDelay); + } + AnimationInfo info = new AnimationInfo(view, getName(), + sceneRoot.getWindowId(), infoValues); runningAnimators.put(animator, info); mAnimators.add(animator); } @@ -504,6 +533,14 @@ public abstract class Transition implements Cloneable { } } } + if (minStartDelay != 0) { + for (int i = 0; i < startDelays.size(); i++) { + int index = startDelays.keyAt(i); + Animator animator = mAnimators.get(index); + long delay = startDelays.valueAt(i) - minStartDelay + animator.getStartDelay(); + animator.setStartDelay(delay); + } + } } /** @@ -563,7 +600,7 @@ public abstract class Transition implements Cloneable { /** * This is called internally once all animations have been set up by the - * transition hierarchy. \ + * transition hierarchy. * * @hide */ @@ -1008,6 +1045,7 @@ public abstract class Transition implements Cloneable { } else { captureEndValues(values); } + capturePropagationValues(values); if (start) { mStartValues.viewValues.put(view, values); if (id >= 0) { @@ -1033,6 +1071,7 @@ public abstract class Transition implements Cloneable { } else { captureEndValues(values); } + capturePropagationValues(values); if (start) { mStartValues.viewValues.put(view, values); } else { @@ -1077,6 +1116,9 @@ public abstract class Transition implements Cloneable { if (view == null) { return; } + if (!isValidTarget(view, view.getId())) { + return; + } boolean isListViewItem = false; if (view.getParent() instanceof ListView) { isListViewItem = true; @@ -1109,30 +1151,33 @@ public abstract class Transition implements Cloneable { } } } - TransitionValues values = new TransitionValues(); - values.view = view; - if (start) { - captureStartValues(values); - } else { - captureEndValues(values); - } - if (start) { - if (!isListViewItem) { - mStartValues.viewValues.put(view, values); - if (id >= 0) { - mStartValues.idValues.put((int) id, values); - } + if (view.getParent() instanceof ViewGroup) { + TransitionValues values = new TransitionValues(); + values.view = view; + if (start) { + captureStartValues(values); } else { - mStartValues.itemIdValues.put(itemId, values); + captureEndValues(values); } - } else { - if (!isListViewItem) { - mEndValues.viewValues.put(view, values); - if (id >= 0) { - mEndValues.idValues.put((int) id, values); + capturePropagationValues(values); + if (start) { + if (!isListViewItem) { + mStartValues.viewValues.put(view, values); + if (id >= 0) { + mStartValues.idValues.put((int) id, values); + } + } else { + mStartValues.itemIdValues.put(itemId, values); } } else { - mEndValues.itemIdValues.put(itemId, values); + if (!isListViewItem) { + mEndValues.viewValues.put(view, values); + if (id >= 0) { + mEndValues.idValues.put((int) id, values); + } + } else { + mEndValues.itemIdValues.put(itemId, values); + } } } if (view instanceof ViewGroup) { @@ -1194,13 +1239,17 @@ public abstract class Transition implements Cloneable { * * @hide */ - public void pause() { + public void pause(View sceneRoot) { if (!mEnded) { ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators(); int numOldAnims = runningAnimators.size(); + WindowId windowId = sceneRoot.getWindowId(); for (int i = numOldAnims - 1; i >= 0; i--) { - Animator anim = runningAnimators.keyAt(i); - anim.pause(); + AnimationInfo info = runningAnimators.valueAt(i); + if (info.view != null && windowId.equals(info.windowId)) { + Animator anim = runningAnimators.keyAt(i); + anim.pause(); + } } if (mListeners != null && mListeners.size() > 0) { ArrayList<TransitionListener> tmpListeners = @@ -1221,14 +1270,18 @@ public abstract class Transition implements Cloneable { * * @hide */ - public void resume() { + public void resume(View sceneRoot) { if (mPaused) { if (!mEnded) { ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators(); int numOldAnims = runningAnimators.size(); + WindowId windowId = sceneRoot.getWindowId(); for (int i = numOldAnims - 1; i >= 0; i--) { - Animator anim = runningAnimators.keyAt(i); - anim.resume(); + AnimationInfo info = runningAnimators.valueAt(i); + if (info.view != null && windowId.equals(info.windowId)) { + Animator anim = runningAnimators.keyAt(i); + anim.resume(); + } } if (mListeners != null && mListeners.size() > 0) { ArrayList<TransitionListener> tmpListeners = @@ -1325,7 +1378,7 @@ public abstract class Transition implements Cloneable { animator.setDuration(getDuration()); } if (getStartDelay() >= 0) { - animator.setStartDelay(getStartDelay()); + animator.setStartDelay(getStartDelay() + animator.getStartDelay()); } if (getInterpolator() != null) { animator.setInterpolator(getInterpolator()); @@ -1458,6 +1511,98 @@ public abstract class Transition implements Cloneable { return this; } + /** + * Sets the callback to use to find the epicenter of a Transition. A null value indicates + * that there is no epicenter in the Transition and getEpicenter() will return null. + * Transitions like {@link android.transition.Explode} use a point or Rect to orient + * the direction of travel. This is called the epicenter of the Transition and is + * typically centered on a touched View. The + * {@link android.transition.Transition.EpicenterCallback} allows a Transition to + * dynamically retrieve the epicenter during a Transition. + * @param epicenterCallback The callback to use to find the epicenter of the Transition. + */ + public void setEpicenterCallback(EpicenterCallback epicenterCallback) { + mEpicenterCallback = epicenterCallback; + } + + /** + * Returns the callback used to find the epicenter of the Transition. + * Transitions like {@link android.transition.Explode} use a point or Rect to orient + * the direction of travel. This is called the epicenter of the Transition and is + * typically centered on a touched View. The + * {@link android.transition.Transition.EpicenterCallback} allows a Transition to + * dynamically retrieve the epicenter during a Transition. + * @return the callback used to find the epicenter of the Transition. + */ + public EpicenterCallback getEpicenterCallback() { + return mEpicenterCallback; + } + + /** + * Returns the epicenter as specified by the + * {@link android.transition.Transition.EpicenterCallback} or null if no callback exists. + * @return the epicenter as specified by the + * {@link android.transition.Transition.EpicenterCallback} or null if no callback exists. + * @see #setEpicenterCallback(android.transition.Transition.EpicenterCallback) + */ + public Rect getEpicenter() { + if (mEpicenterCallback == null) { + return null; + } + return mEpicenterCallback.getEpicenter(this); + } + + /** + * Sets the method for determining Animator start delays. + * When a Transition affects several Views like {@link android.transition.Explode} or + * {@link android.transition.Slide}, there may be a desire to have a "wave-front" effect + * such that the Animator start delay depends on position of the View. The + * TransitionPropagation specifies how the start delays are calculated. + * @param transitionPropagation The class used to determine the start delay of + * Animators created by this Transition. A null value + * indicates that no delay should be used. + */ + public void setPropagation(TransitionPropagation transitionPropagation) { + mPropagation = transitionPropagation; + } + + /** + * Returns the {@link android.transition.TransitionPropagation} used to calculate Animator start + * delays. + * When a Transition affects several Views like {@link android.transition.Explode} or + * {@link android.transition.Slide}, there may be a desire to have a "wave-front" effect + * such that the Animator start delay depends on position of the View. The + * TransitionPropagation specifies how the start delays are calculated. + * @return the {@link android.transition.TransitionPropagation} used to calculate Animator start + * delays. This is null by default. + */ + public TransitionPropagation getPropagation() { + return mPropagation; + } + + /** + * Captures TransitionPropagation values for the given view and the + * hierarchy underneath it. + */ + void capturePropagationValues(TransitionValues transitionValues) { + if (mPropagation != null) { + String[] propertyNames = mPropagation.getPropagationProperties(); + if (propertyNames == null) { + return; + } + boolean containsAll = true; + for (int i = 0; i < propertyNames.length; i++) { + if (!transitionValues.values.containsKey(propertyNames[i])) { + containsAll = false; + break; + } + } + if (!containsAll) { + mPropagation.captureValues(transitionValues); + } + } + } + Transition setSceneRoot(ViewGroup sceneRoot) { mSceneRoot = sceneRoot; return this; @@ -1467,6 +1612,10 @@ public abstract class Transition implements Cloneable { mCanRemoveViews = canRemoveViews; } + public boolean canRemoveViews() { + return mCanRemoveViews; + } + @Override public String toString() { return toString(""); @@ -1629,16 +1778,19 @@ public abstract class Transition implements Cloneable { * animation should be canceled or a new animation noop'd. The structure holds * information about the state that an animation is going to, to be compared to * end state of a new animation. + * @hide */ - private static class AnimationInfo { - View view; + public static class AnimationInfo { + public View view; String name; TransitionValues values; + WindowId windowId; - AnimationInfo(View view, String name, TransitionValues values) { + AnimationInfo(View view, String name, WindowId windowId, TransitionValues values) { this.view = view; this.name = name; this.values = values; + this.windowId = windowId; } } @@ -1688,4 +1840,28 @@ public abstract class Transition implements Cloneable { } } + /** + * Class to get the epicenter of Transition. Use + * {@link #setEpicenterCallback(android.transition.Transition.EpicenterCallback)} to + * set the callback used to calculate the epicenter of the Transition. Override + * {@link #getEpicenter()} to return the rectangular region in screen coordinates of + * the epicenter of the transition. + * @see #setEpicenterCallback(android.transition.Transition.EpicenterCallback) + */ + public static abstract class EpicenterCallback { + + /** + * Implementers must override to return the epicenter of the Transition in screen + * coordinates. Transitions like {@link android.transition.Explode} depend upon + * an epicenter for the Transition. In Explode, Views move toward or away from the + * center of the epicenter Rect along the vector between the epicenter and the center + * of the View appearing and disappearing. Some Transitions, such as + * {@link android.transition.Fade} pay no attention to the epicenter. + * + * @param transition The transition for which the epicenter applies. + * @return The Rect region of the epicenter of <code>transition</code> or null if + * there is no epicenter. + */ + public abstract Rect getEpicenter(Transition transition); + } } diff --git a/core/java/android/transition/TransitionInflater.java b/core/java/android/transition/TransitionInflater.java index 9f77d5e..f675c6a 100644 --- a/core/java/android/transition/TransitionInflater.java +++ b/core/java/android/transition/TransitionInflater.java @@ -145,7 +145,13 @@ public class TransitionInflater { transition = new ChangeBounds(); newTransition = true; } else if ("slide".equals(name)) { - transition = new Slide(); + transition = createSlideTransition(attrs); + newTransition = true; + } else if ("explode".equals(name)) { + transition = new Explode(); + newTransition = true; + } else if ("moveImage".equals(name)) { + transition = new MoveImage(); newTransition = true; } else if ("autoTransition".equals(name)) { transition = new AutoTransition(); @@ -188,6 +194,15 @@ public class TransitionInflater { return transition; } + private Slide createSlideTransition(AttributeSet attrs) { + TypedArray a = mContext.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.Slide); + int edge = a.getInt(com.android.internal.R.styleable.Slide_slideEdge, Slide.BOTTOM); + Slide slide = new Slide(edge); + a.recycle(); + return slide; + } + private void getTargetIds(XmlPullParser parser, AttributeSet attrs, Transition transition) throws XmlPullParserException, IOException { @@ -284,25 +299,27 @@ public class TransitionInflater { com.android.internal.R.styleable.TransitionManager); int transitionId = a.getResourceId( com.android.internal.R.styleable.TransitionManager_transition, -1); - Scene fromScene = null, toScene = null; int fromId = a.getResourceId( com.android.internal.R.styleable.TransitionManager_fromScene, -1); - if (fromId >= 0) fromScene = Scene.getSceneForLayout(sceneRoot, fromId, mContext); + Scene fromScene = (fromId < 0) ? null: Scene.getSceneForLayout(sceneRoot, fromId, mContext); int toId = a.getResourceId( com.android.internal.R.styleable.TransitionManager_toScene, -1); - if (toId >= 0) toScene = Scene.getSceneForLayout(sceneRoot, toId, mContext); + Scene toScene = (toId < 0) ? null : Scene.getSceneForLayout(sceneRoot, toId, mContext); + if (transitionId >= 0) { Transition transition = inflateTransition(transitionId); if (transition != null) { - if (fromScene != null) { - if (toScene == null){ - throw new RuntimeException("No matching toScene for given fromScene " + + if (fromScene == null) { + if (toScene == null) { + throw new RuntimeException("No matching fromScene or toScene " + "for transition ID " + transitionId); } else { - transitionManager.setTransition(fromScene, toScene, transition); + transitionManager.setTransition(toScene, transition); } - } else if (toId >= 0) { - transitionManager.setTransition(toScene, transition); + } else if (toScene == null) { + transitionManager.setExitTransition(fromScene, transition); + } else { + transitionManager.setTransition(fromScene, toScene, transition); } } } diff --git a/core/java/android/transition/TransitionManager.java b/core/java/android/transition/TransitionManager.java index 3bf6790..1614d34 100644 --- a/core/java/android/transition/TransitionManager.java +++ b/core/java/android/transition/TransitionManager.java @@ -67,7 +67,10 @@ public class TransitionManager { private static Transition sDefaultTransition = new AutoTransition(); + private static final String[] EMPTY_STRINGS = new String[0]; + ArrayMap<Scene, Transition> mSceneTransitions = new ArrayMap<Scene, Transition>(); + ArrayMap<Scene, Transition> mExitSceneTransitions = new ArrayMap<Scene, Transition>(); ArrayMap<Scene, ArrayMap<Scene, Transition>> mScenePairTransitions = new ArrayMap<Scene, ArrayMap<Scene, Transition>>(); private static ThreadLocal<WeakReference<ArrayMap<ViewGroup, ArrayList<Transition>>>> @@ -116,6 +119,21 @@ public class TransitionManager { } /** + * Sets a specific transition to occur when the given scene is exited. This + * has the lowest priority -- if a Scene-to-Scene transition or + * Scene enter transition can be applied, it will. + * + * @param scene The scene which, when exited, will cause the given + * transition to run. + * @param transition The transition that will play when the given scene is + * exited. A value of null will result in the default behavior of + * using the default transition instead. + */ + public void setExitTransition(Scene scene, Transition transition) { + mExitSceneTransitions.put(scene, transition); + } + + /** * Sets a specific transition to occur when the given pair of scenes is * exited/entered. * @@ -163,6 +181,9 @@ public class TransitionManager { } } transition = mSceneTransitions.get(scene); + if (transition == null && sceneRoot != null) { + transition = mExitSceneTransitions.get(Scene.getCurrentScene(sceneRoot)); + } return (transition != null) ? transition : sDefaultTransition; } @@ -218,6 +239,34 @@ public class TransitionManager { } /** + * Retrieve the transition to a target defined scene if one has been + * associated with this TransitionManager. + * + * @param toScene Target scene that this transition will move to + * @return Transition corresponding to the given toScene or null + * if no association exists in this TransitionManager + * + * @see #setTransition(Scene, Transition) + * @hide + */ + public Transition getEnterTransition(Scene toScene) { + return mSceneTransitions.get(toScene); + } + + /** + * Retrieve the transition from a defined scene to a target named scene if one has been + * associated with this TransitionManager. + * + * @param fromScene Scene that this transition starts from + * @return Transition corresponding to the given fromScene or null + * if no association exists in this TransitionManager + * @hide + */ + public Transition getExitTransition(Scene fromScene) { + return mExitSceneTransitions.get(fromScene); + } + + /** * This private utility class is used to listen for both OnPreDraw and * OnAttachStateChange events. OnPreDraw events are the main ones we care * about since that's what triggers the transition to take place. @@ -253,7 +302,7 @@ public class TransitionManager { ArrayList<Transition> runningTransitions = getRunningTransitions().get(mSceneRoot); if (runningTransitions != null && runningTransitions.size() > 0) { for (Transition runningTransition : runningTransitions) { - runningTransition.resume(); + runningTransition.resume(mSceneRoot); } } mTransition.clearValues(true); @@ -286,7 +335,7 @@ public class TransitionManager { mTransition.captureValues(mSceneRoot, false); if (previousRunningTransitions != null) { for (Transition runningTransition : previousRunningTransitions) { - runningTransition.resume(); + runningTransition.resume(mSceneRoot); } } mTransition.playTransition(mSceneRoot); @@ -302,7 +351,7 @@ public class TransitionManager { if (runningTransitions != null && runningTransitions.size() > 0) { for (Transition runningTransition : runningTransitions) { - runningTransition.pause(); + runningTransition.pause(sceneRoot); } } @@ -329,7 +378,6 @@ public class TransitionManager { // Auto transition if there is no transition declared for the Scene, but there is // a root or parent view changeScene(scene, getTransition(scene)); - } /** diff --git a/core/java/android/transition/TransitionPropagation.java b/core/java/android/transition/TransitionPropagation.java new file mode 100644 index 0000000..9a481c2 --- /dev/null +++ b/core/java/android/transition/TransitionPropagation.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2014 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.transition; + +import android.graphics.Rect; +import android.view.ViewGroup; + +/** + * Extend <code>TransitionPropagation</code> to customize start delays for Animators created + * in {@link android.transition.Transition#createAnimator(ViewGroup, + * TransitionValues, TransitionValues)}. A Transition such as {@link android.transition.Explode} + * defaults to using {@link android.transition.CircularPropagation} and Views closer to the + * epicenter will move out of the scene later and into the scene sooner than Views farther + * from the epicenter, giving the appearance of inertia. With no TransitionPropagation, all + * Views will react simultaneously to the start of the transition. + * + * @see Transition#setPropagation(TransitionPropagation) + * @see Transition#getEpicenter() + */ +public abstract class TransitionPropagation { + /** + * Called by Transition to alter the Animator start delay. All start delays will be adjusted + * such that the minimum becomes zero. + * @param sceneRoot The root of the View hierarchy running the transition. + * @param transition The transition that created the Animator + * @param startValues The values for a specific target in the start scene. + * @param endValues The values for the target in the end scene. + * @return A start delay to use with the Animator created by <code>transition</code>. The + * delay will be offset by the minimum delay of all <code>TransitionPropagation</code>s + * used in the Transition so that the smallest delay will be 0. Returned values may be + * negative. + */ + public abstract long getStartDelay(ViewGroup sceneRoot, Transition transition, + TransitionValues startValues, TransitionValues endValues); + + /** + * Captures the values in the start or end scene for the properties that this + * transition propagation monitors. These values are then passed as the startValues + * or endValues structure in a later call to + * {@link #getStartDelay(ViewGroup, Transition, TransitionValues, TransitionValues)}. + * The main concern for an implementation is what the + * properties are that the transition cares about and what the values are + * for all of those properties. The start and end values will be compared + * later during the + * {@link #getStartDelay(ViewGroup, Transition, TransitionValues, TransitionValues)}. + * method to determine the start delay. + * + * <p>Subclasses must implement this method. The method should only be called by the + * transition system; it is not intended to be called from external classes.</p> + * + * @param transitionValues The holder for any values that the Transition + * wishes to store. Values are stored in the <code>values</code> field + * of this TransitionValues object and are keyed from + * a String value. For example, to store a view's rotation value, + * a transition might call + * <code>transitionValues.values.put("appname:transitionname:rotation", + * view.getRotation())</code>. The target view will already be stored in + * the transitionValues structure when this method is called. + */ + public abstract void captureValues(TransitionValues transitionValues); + + /** + * Returns the set of property names stored in the {@link TransitionValues} + * object passed into {@link #captureValues(TransitionValues)} that + * this transition propagation cares about for the purposes of preventing + * duplicate capturing of property values. + + * <p>A <code>TransitionPropagation</code> must override this method to prevent + * duplicate capturing of values and must contain at least one </p> + * + * @return An array of property names as described in the class documentation for + * {@link TransitionValues}. + */ + public abstract String[] getPropagationProperties() ; +} diff --git a/core/java/android/transition/TransitionSet.java b/core/java/android/transition/TransitionSet.java index 4545e3b..966b24d 100644 --- a/core/java/android/transition/TransitionSet.java +++ b/core/java/android/transition/TransitionSet.java @@ -17,6 +17,7 @@ package android.transition; import android.animation.TimeInterpolator; +import android.graphics.Rect; import android.util.AndroidRuntimeException; import android.view.View; import android.view.ViewGroup; @@ -315,23 +316,32 @@ public class TransitionSet extends Transition { } } + @Override + void capturePropagationValues(TransitionValues transitionValues) { + super.capturePropagationValues(transitionValues); + int numTransitions = mTransitions.size(); + for (int i = 0; i < numTransitions; ++i) { + mTransitions.get(i).capturePropagationValues(transitionValues); + } + } + /** @hide */ @Override - public void pause() { - super.pause(); + public void pause(View sceneRoot) { + super.pause(sceneRoot); int numTransitions = mTransitions.size(); for (int i = 0; i < numTransitions; ++i) { - mTransitions.get(i).pause(); + mTransitions.get(i).pause(sceneRoot); } } /** @hide */ @Override - public void resume() { - super.resume(); + public void resume(View sceneRoot) { + super.resume(sceneRoot); int numTransitions = mTransitions.size(); for (int i = 0; i < numTransitions; ++i) { - mTransitions.get(i).resume(); + mTransitions.get(i).resume(sceneRoot); } } @@ -365,6 +375,24 @@ public class TransitionSet extends Transition { } @Override + public void setPropagation(TransitionPropagation propagation) { + super.setPropagation(propagation); + int numTransitions = mTransitions.size(); + for (int i = 0; i < numTransitions; ++i) { + mTransitions.get(i).setPropagation(propagation); + } + } + + @Override + public void setEpicenterCallback(EpicenterCallback epicenterCallback) { + super.setEpicenterCallback(epicenterCallback); + int numTransitions = mTransitions.size(); + for (int i = 0; i < numTransitions; ++i) { + mTransitions.get(i).setEpicenterCallback(epicenterCallback); + } + } + + @Override String toString(String indent) { String result = super.toString(indent); for (int i = 0; i < mTransitions.size(); ++i) { @@ -383,5 +411,4 @@ public class TransitionSet extends Transition { } return clone; } - } diff --git a/core/java/android/transition/Visibility.java b/core/java/android/transition/Visibility.java index 44f92cd..7783b6f 100644 --- a/core/java/android/transition/Visibility.java +++ b/core/java/android/transition/Visibility.java @@ -17,6 +17,7 @@ package android.transition; import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.view.View; import android.view.ViewGroup; @@ -29,15 +30,20 @@ import android.view.ViewGroup; * information to determine the specific animations to run when visibility * changes occur. Subclasses should implement one or both of the methods * {@link #onAppear(ViewGroup, TransitionValues, int, TransitionValues, int)}, - * {@link #onDisappear(ViewGroup, TransitionValues, int, TransitionValues, int)}, + * {@link #onDisappear(ViewGroup, TransitionValues, int, TransitionValues, int)} or + * {@link #onAppear(ViewGroup, View, TransitionValues, TransitionValues)}, + * {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)}. */ public abstract class Visibility extends Transition { private static final String PROPNAME_VISIBILITY = "android:visibility:visibility"; private static final String PROPNAME_PARENT = "android:visibility:parent"; + private static final String PROPNAME_SCREEN_LOCATION = "android:visibility:screenLocation"; + private static final String[] sTransitionProperties = { PROPNAME_VISIBILITY, PROPNAME_PARENT, + PROPNAME_SCREEN_LOCATION, }; private static class VisibilityInfo { @@ -58,6 +64,9 @@ public abstract class Visibility extends Transition { int visibility = transitionValues.view.getVisibility(); transitionValues.values.put(PROPNAME_VISIBILITY, visibility); transitionValues.values.put(PROPNAME_PARENT, transitionValues.view.getParent()); + int[] loc = new int[2]; + transitionValues.view.getLocationOnScreen(loc); + transitionValues.values.put(PROPNAME_SCREEN_LOCATION, loc); } @Override @@ -179,8 +188,11 @@ public abstract class Visibility extends Transition { } /** - * The default implementation of this method does nothing. Subclasses - * should override if they need to create an Animator when targets appear. + * The default implementation of this method calls + * {@link #onAppear(ViewGroup, View, TransitionValues, TransitionValues)}. + * Subclasses should override this method or + * {@link #onAppear(ViewGroup, View, TransitionValues, TransitionValues)}. + * if they need to create an Animator when targets appear. * The method should only be called by the Visibility class; it is * not intended to be called from external classes. * @@ -196,15 +208,53 @@ public abstract class Visibility extends Transition { public Animator onAppear(ViewGroup sceneRoot, TransitionValues startValues, int startVisibility, TransitionValues endValues, int endVisibility) { + return onAppear(sceneRoot, endValues.view, startValues, endValues); + } + + /** + * The default implementation of this method returns a null Animator. Subclasses should + * override this method to make targets appear with the desired transition. The + * method should only be called from + * {@link #onAppear(ViewGroup, TransitionValues, int, TransitionValues, int)}. + * + * @param sceneRoot The root of the transition hierarchy + * @param view The View to make appear. This will be in the target scene's View hierarchy and + * will be VISIBLE. + * @param startValues The target values in the start scene + * @param endValues The target values in the end scene + * @return An Animator to be started at the appropriate time in the + * overall transition for this scene change. A null value means no animation + * should be run. + */ + public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, + TransitionValues endValues) { return null; } /** - * The default implementation of this method does nothing. Subclasses - * should override if they need to create an Animator when targets disappear. + * Subclasses should override this method or + * {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)} + * if they need to create an Animator when targets disappear. * The method should only be called by the Visibility class; it is * not intended to be called from external classes. - * + * <p> + * The default implementation of this method attempts to find a View to use to call + * {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)}, + * based on the situation of the View in the View hierarchy. For example, + * if a View was simply removed from its parent, then the View will be added + * into a {@link android.view.ViewGroupOverlay} and passed as the <code>view</code> + * parameter in {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)}. + * If a visible View is changed to be {@link View#GONE} or {@link View#INVISIBLE}, + * then it can be used as the <code>view</code> and the visibility will be changed + * to {@link View#VISIBLE} for the duration of the animation. However, if a View + * is in a hierarchy which is also altering its visibility, the situation can be + * more complicated. In general, if a view that is no longer in the hierarchy in + * the end scene still has a parent (so its parent hierarchy was removed, but it + * was not removed from its parent), then it will be left alone to avoid side-effects from + * improperly removing it from its parent. The only exception to this is if + * the previous {@link Scene} was {@link Scene#getSceneForLayout(ViewGroup, int, + * android.content.Context) created from a layout resource file}, then it is considered + * safe to un-parent the starting scene view in order to make it disappear.</p> * * @param sceneRoot The root of the transition hierarchy * @param startValues The target values in the start scene @@ -218,6 +268,144 @@ public abstract class Visibility extends Transition { public Animator onDisappear(ViewGroup sceneRoot, TransitionValues startValues, int startVisibility, TransitionValues endValues, int endVisibility) { + View startView = (startValues != null) ? startValues.view : null; + View endView = (endValues != null) ? endValues.view : null; + View overlayView = null; + View viewToKeep = null; + if (endView == null || endView.getParent() == null) { + if (endView != null) { + // endView was removed from its parent - add it to the overlay + overlayView = endView; + } else if (startView != null) { + // endView does not exist. Use startView only under certain + // conditions, because placing a view in an overlay necessitates + // it being removed from its current parent + if (startView.getParent() == null) { + // no parent - safe to use + overlayView = startView; + } else if (startView.getParent() instanceof View && + startView.getParent().getParent() == null) { + View startParent = (View) startView.getParent(); + int id = startParent.getId(); + if (id != View.NO_ID && sceneRoot.findViewById(id) != null && mCanRemoveViews) { + // no parent, but its parent is unparented but the parent + // hierarchy has been replaced by a new hierarchy with the same id + // and it is safe to un-parent startView + overlayView = startView; + } + } + } + } else { + // visibility change + if (endVisibility == View.INVISIBLE) { + viewToKeep = endView; + } else { + // Becoming GONE + if (startView == endView) { + viewToKeep = endView; + } else { + overlayView = startView; + } + } + } + final int finalVisibility = endVisibility; + final ViewGroup finalSceneRoot = sceneRoot; + + if (overlayView != null) { + // TODO: Need to do this for general case of adding to overlay + int[] screenLoc = (int[]) startValues.values.get(PROPNAME_SCREEN_LOCATION); + int screenX = screenLoc[0]; + int screenY = screenLoc[1]; + int[] loc = new int[2]; + sceneRoot.getLocationOnScreen(loc); + overlayView.offsetLeftAndRight((screenX - loc[0]) - overlayView.getLeft()); + overlayView.offsetTopAndBottom((screenY - loc[1]) - overlayView.getTop()); + sceneRoot.getOverlay().add(overlayView); + Animator animator = onDisappear(sceneRoot, overlayView, startValues, endValues); + if (animator == null) { + sceneRoot.getOverlay().remove(overlayView); + } else { + final View finalOverlayView = overlayView; + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + finalSceneRoot.getOverlay().remove(finalOverlayView); + } + + @Override + public void onAnimationPause(Animator animation) { + finalSceneRoot.getOverlay().remove(finalOverlayView); + } + + @Override + public void onAnimationResume(Animator animation) { + finalSceneRoot.getOverlay().add(finalOverlayView); + } + }); + } + return animator; + } + + if (viewToKeep != null) { + viewToKeep.setVisibility(View.VISIBLE); + Animator animator = onDisappear(sceneRoot, viewToKeep, startValues, endValues); + if (animator == null) { + viewToKeep.setVisibility(finalVisibility); + } else { + final View finalViewToKeep = viewToKeep; + animator.addListener(new AnimatorListenerAdapter() { + boolean mCanceled = false; + + @Override + public void onAnimationPause(Animator animation) { + if (!mCanceled) { + finalViewToKeep.setVisibility(finalVisibility); + } + } + + @Override + public void onAnimationResume(Animator animation) { + if (!mCanceled) { + finalViewToKeep.setVisibility(View.VISIBLE); + } + } + + @Override + public void onAnimationCancel(Animator animation) { + mCanceled = true; + } + + @Override + public void onAnimationEnd(Animator animation) { + if (!mCanceled) { + finalViewToKeep.setVisibility(finalVisibility); + } + } + }); + } + return animator; + } + return null; + } + + /** + * The default implementation of this method returns a null Animator. Subclasses should + * override this method to make targets disappear with the desired transition. The + * method should only be called from + * {@link #onDisappear(ViewGroup, TransitionValues, int, TransitionValues, int)}. + * + * @param sceneRoot The root of the transition hierarchy + * @param view The View to make disappear. This will be in the target scene's View + * hierarchy or in an {@link android.view.ViewGroupOverlay} and will be + * VISIBLE. + * @param startValues The target values in the start scene + * @param endValues The target values in the end scene + * @return An Animator to be started at the appropriate time in the + * overall transition for this scene change. A null value means no animation + * should be run. + */ + public Animator onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues, + TransitionValues endValues) { return null; } } diff --git a/core/java/android/transition/VisibilityPropagation.java b/core/java/android/transition/VisibilityPropagation.java new file mode 100644 index 0000000..0326d47 --- /dev/null +++ b/core/java/android/transition/VisibilityPropagation.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2014 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.transition; + +import android.view.View; + +/** + * Base class for <code>TransitionPropagation</code>s that care about + * View Visibility and the center position of the View. + */ +public abstract class VisibilityPropagation extends TransitionPropagation { + + /** + * The property key used for {@link android.view.View#getVisibility()}. + */ + private static final String PROPNAME_VISIBILITY = "android:visibilityPropagation:visibility"; + + /** + * The property key used for the center of the View in screen coordinates. This is an + * int[2] with the index 0 taking the x coordinate and index 1 taking the y coordinate. + */ + private static final String PROPNAME_VIEW_CENTER = "android:visibilityPropagation:center"; + + private static final String[] VISIBILITY_PROPAGATION_VALUES = { + PROPNAME_VISIBILITY, + PROPNAME_VIEW_CENTER, + }; + + @Override + public void captureValues(TransitionValues values) { + View view = values.view; + values.values.put(PROPNAME_VISIBILITY, view.getVisibility()); + int[] loc = new int[2]; + view.getLocationOnScreen(loc); + loc[0] += Math.round(view.getTranslationX()); + loc[0] += view.getWidth() / 2; + loc[1] += Math.round(view.getTranslationY()); + loc[1] += view.getHeight() / 2; + values.values.put(PROPNAME_VIEW_CENTER, loc); + } + + @Override + public String[] getPropagationProperties() { + return VISIBILITY_PROPAGATION_VALUES; + } + + /** + * Returns {@link android.view.View#getVisibility()} for the View at the time the values + * were captured. + * @param values The TransitionValues captured at the start or end of the Transition. + * @return {@link android.view.View#getVisibility()} for the View at the time the values + * were captured. + */ + public int getViewVisibility(TransitionValues values) { + if (values == null) { + return View.GONE; + } + Integer visibility = (Integer) values.values.get(PROPNAME_VISIBILITY); + if (visibility == null) { + return View.GONE; + } + return visibility; + } + + /** + * Returns the View's center x coordinate, relative to the screen, at the time the values + * were captured. + * @param values The TransitionValues captured at the start or end of the Transition. + * @return the View's center x coordinate, relative to the screen, at the time the values + * were captured. + */ + public int getViewX(TransitionValues values) { + return getViewCoordinate(values, 0); + } + + /** + * Returns the View's center y coordinate, relative to the screen, at the time the values + * were captured. + * @param values The TransitionValues captured at the start or end of the Transition. + * @return the View's center y coordinate, relative to the screen, at the time the values + * were captured. + */ + public int getViewY(TransitionValues values) { + return getViewCoordinate(values, 1); + } + + private static int getViewCoordinate(TransitionValues values, int coordinateIndex) { + if (values == null) { + return -1; + } + + int[] coordinates = (int[]) values.values.get(PROPNAME_VIEW_CENTER); + if (coordinates == null) { + return -1; + } + + return coordinates[coordinateIndex]; + } +} diff --git a/core/java/android/tv/ITvInputClient.aidl b/core/java/android/tv/ITvInputClient.aidl new file mode 100644 index 0000000..43be6f0 --- /dev/null +++ b/core/java/android/tv/ITvInputClient.aidl @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2014 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.tv; + +import android.content.ComponentName; +import android.tv.ITvInputSession; + +/** + * Interface a client of the ITvInputManager implements, to identify itself and receive information + * about changes to the state of each TV input service. + * @hide + */ +oneway interface ITvInputClient { + void onSessionCreated(in ComponentName name, IBinder token, int seq); + void onAvailabilityChanged(in ComponentName name, boolean isAvailable); +} diff --git a/core/java/android/tv/ITvInputManager.aidl b/core/java/android/tv/ITvInputManager.aidl new file mode 100644 index 0000000..a927dc9 --- /dev/null +++ b/core/java/android/tv/ITvInputManager.aidl @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2014 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.tv; + +import android.content.ComponentName; +import android.net.Uri; +import android.tv.ITvInputClient; +import android.tv.TvInputInfo; +import android.view.Surface; + +/** + * Interface to the TV input manager service. + * @hide + */ +interface ITvInputManager { + List<TvInputInfo> getTvInputList(int userId); + + boolean getAvailability(in ITvInputClient client, in ComponentName name, int userId); + + void registerCallback(in ITvInputClient client, in ComponentName name, int userId); + void unregisterCallback(in ITvInputClient client, in ComponentName name, int userId); + + void createSession(in ITvInputClient client, in ComponentName name, int seq, int userId); + void releaseSession(in IBinder sessionToken, int userId); + + void setSurface(in IBinder sessionToken, in Surface surface, int userId); + void setVolume(in IBinder sessionToken, float volume, int userId); + void tune(in IBinder sessionToken, in Uri channelUri, int userId); +} diff --git a/core/java/android/tv/ITvInputService.aidl b/core/java/android/tv/ITvInputService.aidl new file mode 100644 index 0000000..d80f286 --- /dev/null +++ b/core/java/android/tv/ITvInputService.aidl @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2014 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.tv; + +import android.tv.ITvInputServiceCallback; +import android.tv.ITvInputSession; +import android.tv.ITvInputSessionCallback; + +/** + * Top-level interface to a TV input component (implemented in a Service). + * @hide + */ +oneway interface ITvInputService { + void registerCallback(ITvInputServiceCallback callback); + void unregisterCallback(in ITvInputServiceCallback callback); + void createSession(ITvInputSessionCallback callback); +} diff --git a/core/java/android/tv/ITvInputServiceCallback.aidl b/core/java/android/tv/ITvInputServiceCallback.aidl new file mode 100644 index 0000000..e535c81 --- /dev/null +++ b/core/java/android/tv/ITvInputServiceCallback.aidl @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2014 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.tv; + +import android.content.ComponentName; + +/** + * Helper interface for ITvInputService to allow the TV input to notify the client when its status + * has been changed. + * @hide + */ +oneway interface ITvInputServiceCallback { + void onAvailabilityChanged(in ComponentName name, boolean isAvailable); +} diff --git a/core/java/android/tv/ITvInputSession.aidl b/core/java/android/tv/ITvInputSession.aidl new file mode 100644 index 0000000..d379d2d --- /dev/null +++ b/core/java/android/tv/ITvInputSession.aidl @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2014 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.tv; + +import android.net.Uri; +import android.view.Surface; + +/** + * Sub-interface of ITvInputService which is created per session and has its own context. + * @hide + */ +oneway interface ITvInputSession { + void release(); + + void setSurface(in Surface surface); + // TODO: Remove this once it becomes irrelevant for applications to handle audio focus. The plan + // is to introduce some new concepts that will solve a number of problems in audio policy today. + void setVolume(float volume); + void tune(in Uri channelUri); +} diff --git a/core/java/android/tv/ITvInputSessionCallback.aidl b/core/java/android/tv/ITvInputSessionCallback.aidl new file mode 100644 index 0000000..a2bd0d7 --- /dev/null +++ b/core/java/android/tv/ITvInputSessionCallback.aidl @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2014 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.tv; + +import android.tv.ITvInputSession; + +/** + * Helper interface for ITvInputSession to allow the TV input to notify the system service when a + * new session has been created. + * @hide + */ +oneway interface ITvInputSessionCallback { + void onSessionCreated(ITvInputSession session); +} diff --git a/core/java/android/tv/ITvInputSessionWrapper.java b/core/java/android/tv/ITvInputSessionWrapper.java new file mode 100644 index 0000000..66fe5e1 --- /dev/null +++ b/core/java/android/tv/ITvInputSessionWrapper.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2014 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.tv; + +import android.content.Context; +import android.net.Uri; +import android.os.Message; +import android.tv.TvInputService.TvInputSessionImpl; +import android.util.Log; +import android.view.Surface; + +import com.android.internal.os.HandlerCaller; + +/** + * Implements the internal ITvInputSession interface to convert incoming calls on to it back to + * calls on the public TvInputSession interface, scheduling them on the main thread of the process. + * + * @hide + */ +public class ITvInputSessionWrapper extends ITvInputSession.Stub implements HandlerCaller.Callback { + private static final String TAG = "TvInputSessionWrapper"; + + private static final int DO_RELEASE = 1; + private static final int DO_SET_SURFACE = 2; + private static final int DO_SET_VOLUME = 3; + private static final int DO_TUNE = 4; + + private TvInputSessionImpl mTvInputSession; + private final HandlerCaller mCaller; + + public ITvInputSessionWrapper(Context context, TvInputSessionImpl session) { + mCaller = new HandlerCaller(context, null, this, true /* asyncHandler */); + mTvInputSession = session; + } + + @Override + public void executeMessage(Message msg) { + if (mTvInputSession == null) { + return; + } + + switch (msg.what) { + case DO_RELEASE: { + mTvInputSession.release(); + mTvInputSession = null; + return; + } + case DO_SET_SURFACE: { + mTvInputSession.setSurface((Surface) msg.obj); + return; + } + case DO_SET_VOLUME: { + mTvInputSession.setVolume((Float) msg.obj); + return; + } + case DO_TUNE: { + mTvInputSession.tune((Uri) msg.obj); + return; + } + default: { + Log.w(TAG, "Unhandled message code: " + msg.what); + return; + } + } + } + + @Override + public void release() { + mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_RELEASE)); + } + + @Override + public void setSurface(Surface surface) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SET_SURFACE, surface)); + } + + @Override + public final void setVolume(float volume) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SET_VOLUME, volume)); + } + + @Override + public void tune(Uri channelUri) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_TUNE, channelUri)); + } +} diff --git a/core/java/android/tv/TvInputInfo.aidl b/core/java/android/tv/TvInputInfo.aidl new file mode 100644 index 0000000..abc4b47 --- /dev/null +++ b/core/java/android/tv/TvInputInfo.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2014 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.tv; + +parcelable TvInputInfo; diff --git a/core/java/android/tv/TvInputInfo.java b/core/java/android/tv/TvInputInfo.java new file mode 100644 index 0000000..90e4177 --- /dev/null +++ b/core/java/android/tv/TvInputInfo.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2014 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.tv; + +import android.content.ComponentName; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * This class is used to specify meta information of a TV input. + */ +public final class TvInputInfo implements Parcelable { + private final ResolveInfo mService; + private final String mId; + + /** + * Constructor. + * + * @param service The ResolveInfo returned from the package manager about this TV input service. + * @hide + */ + public TvInputInfo(ResolveInfo service) { + mService = service; + ServiceInfo si = service.serviceInfo; + mId = new ComponentName(si.packageName, si.name).flattenToShortString(); + } + + /** + * Returns a unique ID for this TV input. The ID is generated from the package and class name + * implementing the TV input service. + */ + public String getId() { + return mId; + } + + /** + * Returns the .apk package that implements this TV input service. + */ + public String getPackageName() { + return mService.serviceInfo.packageName; + } + + /** + * Returns the class name of the service component that implements this TV input service. + */ + public String getServiceName() { + return mService.serviceInfo.name; + } + + /** + * Returns the component of the service that implements this TV input. + */ + public ComponentName getComponent() { + return new ComponentName(mService.serviceInfo.packageName, mService.serviceInfo.name); + } + + /** + * Loads the user-displayed label for this TV input service. + * + * @param pm Supplies a PackageManager used to load the TV input's resources. + * @return Returns a CharSequence containing the TV input's label. If the TV input does not have + * a label, its name is returned. + */ + public CharSequence loadLabel(PackageManager pm) { + return mService.loadLabel(pm); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public int hashCode() { + return mId.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + + if (!(o instanceof TvInputInfo)) { + return false; + } + + TvInputInfo obj = (TvInputInfo) o; + return mId.equals(obj.mId) + && mService.serviceInfo.packageName.equals(obj.mService.serviceInfo.packageName) + && mService.serviceInfo.name.equals(obj.mService.serviceInfo.name); + } + + @Override + public String toString() { + return "TvInputInfo{id=" + mId + + ", pkg=" + mService.serviceInfo.packageName + + ", service=" + mService.serviceInfo.name + "}"; + } + + /** + * Used to package this object into a {@link Parcel}. + * + * @param dest The {@link Parcel} to be written. + * @param flags The flags used for parceling. + */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mId); + mService.writeToParcel(dest, flags); + } + + /** + * Used to make this class parcelable. + * + * @hide + */ + public static final Parcelable.Creator<TvInputInfo> CREATOR = + new Parcelable.Creator<TvInputInfo>() { + @Override + public TvInputInfo createFromParcel(Parcel in) { + return new TvInputInfo(in); + } + + @Override + public TvInputInfo[] newArray(int size) { + return new TvInputInfo[size]; + } + }; + + private TvInputInfo(Parcel in) { + mId = in.readString(); + mService = ResolveInfo.CREATOR.createFromParcel(in); + } +} diff --git a/core/java/android/tv/TvInputManager.java b/core/java/android/tv/TvInputManager.java new file mode 100644 index 0000000..4cf2b35 --- /dev/null +++ b/core/java/android/tv/TvInputManager.java @@ -0,0 +1,392 @@ +/* + * Copyright (C) 2014 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.tv; + +import android.content.ComponentName; +import android.net.Uri; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; +import android.util.SparseArray; +import android.view.Surface; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * Central system API to the overall TV input framework (TIF) architecture, which arbitrates + * interaction between applications and the selected TV inputs. + */ +public final class TvInputManager { + private static final String TAG = "TvInputManager"; + + private final ITvInputManager mService; + + // A mapping from an input to the list of its TvInputListenerRecords. + private final Map<ComponentName, List<TvInputListenerRecord>> mTvInputListenerRecordsMap = + new HashMap<ComponentName, List<TvInputListenerRecord>>(); + + // A mapping from the sequence number of a session to its SessionCreateCallbackRecord. + private final SparseArray<SessionCreateCallbackRecord> mSessionCreateCallbackRecordMap = + new SparseArray<SessionCreateCallbackRecord>(); + + // A sequence number for the next session to be created. Should be protected by a lock + // {@code mSessionCreateCallbackRecordMap}. + private int mNextSeq; + + private final ITvInputClient mClient; + + private final int mUserId; + + /** + * Interface used to receive the created session. + */ + public interface SessionCreateCallback { + /** + * This is called after {@link TvInputManager#createSession} has been processed. + * + * @param session A {@link TvInputManager.Session} instance created. This can be + * {@code null} if the creation request failed. + */ + void onSessionCreated(Session session); + } + + private static final class SessionCreateCallbackRecord { + private final SessionCreateCallback mSessionCreateCallback; + private final Handler mHandler; + + public SessionCreateCallbackRecord(SessionCreateCallback sessionCreateCallback, + Handler handler) { + mSessionCreateCallback = sessionCreateCallback; + mHandler = handler; + } + + public void postSessionCreated(final Session session) { + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCreateCallback.onSessionCreated(session); + } + }); + } + } + + /** + * Interface used to monitor status of the TV input. + */ + public abstract static class TvInputListener { + /** + * This is called when the availability status of a given TV input is changed. + * + * @param name {@link ComponentName} of {@link android.app.Service} that implements the + * given TV input. + * @param isAvailable {@code true} if the given TV input is available to show TV programs. + * {@code false} otherwise. + */ + public void onAvailabilityChanged(ComponentName name, boolean isAvailable) { + } + } + + private static final class TvInputListenerRecord { + private final TvInputListener mListener; + private final Handler mHandler; + + public TvInputListenerRecord(TvInputListener listener, Handler handler) { + mListener = listener; + mHandler = handler; + } + + public TvInputListener getListener() { + return mListener; + } + + public void postAvailabilityChanged(final ComponentName name, final boolean isAvailable) { + mHandler.post(new Runnable() { + @Override + public void run() { + mListener.onAvailabilityChanged(name, isAvailable); + } + }); + } + } + + /** + * @hide + */ + public TvInputManager(ITvInputManager service, int userId) { + mService = service; + mUserId = userId; + mClient = new ITvInputClient.Stub() { + @Override + public void onSessionCreated(ComponentName name, IBinder token, int seq) { + synchronized (mSessionCreateCallbackRecordMap) { + SessionCreateCallbackRecord record = mSessionCreateCallbackRecordMap.get(seq); + mSessionCreateCallbackRecordMap.delete(seq); + if (record == null) { + Log.e(TAG, "Callback not found for " + token); + return; + } + Session session = null; + if (token != null) { + session = new Session(name, token, mService, mUserId); + } + record.postSessionCreated(session); + } + } + + @Override + public void onAvailabilityChanged(ComponentName name, boolean isAvailable) { + synchronized (mTvInputListenerRecordsMap) { + List<TvInputListenerRecord> records = mTvInputListenerRecordsMap.get(name); + if (records == null) { + // Silently ignore - no listener is registered yet. + return; + } + int recordsCount = records.size(); + for (int i = 0; i < recordsCount; i++) { + records.get(i).postAvailabilityChanged(name, isAvailable); + } + } + } + }; + } + + /** + * Returns the complete list of TV inputs on the system. + * + * @return List of {@link TvInputInfo} for each TV input that describes its meta information. + */ + public List<TvInputInfo> getTvInputList() { + try { + return mService.getTvInputList(mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** + * Returns the availability of a given TV input. + * + * @param name {@link ComponentName} of {@link android.app.Service} that implements the given TV + * input. + * @throws IllegalArgumentException if the argument is {@code null}. + * @throws IllegalStateException If there is no {@link TvInputListener} registered on the given + * TV input. + */ + public boolean getAvailability(ComponentName name) { + if (name == null) { + throw new IllegalArgumentException("name cannot be null"); + } + synchronized (mTvInputListenerRecordsMap) { + List<TvInputListenerRecord> records = mTvInputListenerRecordsMap.get(name); + if (records == null || records.size() == 0) { + throw new IllegalStateException("At least one listener should be registered."); + } + } + try { + return mService.getAvailability(mClient, name, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** + * Registers a {@link TvInputListener} for a given TV input. + * + * @param name {@link ComponentName} of {@link android.app.Service} that implements the given TV + * input. + * @param listener a listener used to monitor status of the given TV input. + * @param handler a {@link Handler} that the status change will be delivered to. + * @throws IllegalArgumentException if any of the arguments is {@code null}. + */ + public void registerListener(ComponentName name, TvInputListener listener, Handler handler) { + if (name == null) { + throw new IllegalArgumentException("name cannot be null"); + } + if (listener == null) { + throw new IllegalArgumentException("listener cannot be null"); + } + if (handler == null) { + throw new IllegalArgumentException("handler cannot be null"); + } + synchronized (mTvInputListenerRecordsMap) { + List<TvInputListenerRecord> records = mTvInputListenerRecordsMap.get(name); + if (records == null) { + records = new ArrayList<TvInputListenerRecord>(); + mTvInputListenerRecordsMap.put(name, records); + try { + mService.registerCallback(mClient, name, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + records.add(new TvInputListenerRecord(listener, handler)); + } + } + + /** + * Unregisters the existing {@link TvInputListener} for a given TV input. + * + * @param name {@link ComponentName} of {@link android.app.Service} that implements the given TV + * input. + * @param listener the existing listener to remove for the given TV input. + * @throws IllegalArgumentException if any of the arguments is {@code null}. + */ + public void unregisterListener(ComponentName name, final TvInputListener listener) { + if (name == null) { + throw new IllegalArgumentException("name cannot be null"); + } + if (listener == null) { + throw new IllegalArgumentException("listener cannot be null"); + } + synchronized (mTvInputListenerRecordsMap) { + List<TvInputListenerRecord> records = mTvInputListenerRecordsMap.get(name); + if (records == null) { + Log.e(TAG, "No listener found for " + name.getClassName()); + return; + } + for (Iterator<TvInputListenerRecord> it = records.iterator(); it.hasNext();) { + TvInputListenerRecord record = it.next(); + if (record.getListener() == listener) { + it.remove(); + } + } + if (records.isEmpty()) { + try { + mService.unregisterCallback(mClient, name, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } finally { + mTvInputListenerRecordsMap.remove(name); + } + } + } + } + + /** + * Creates a {@link Session} for a given TV input. + * <p> + * The number of sessions that can be created at the same time is limited by the capability of + * the given TV input. + * </p> + * + * @param name {@link ComponentName} of {@link android.app.Service} that implements the given TV + * input. + * @param callback a callback used to receive the created session. + * @param handler a {@link Handler} that the session creation will be delivered to. + * @throws IllegalArgumentException if any of the arguments is {@code null}. + */ + public void createSession(ComponentName name, final SessionCreateCallback callback, + Handler handler) { + if (name == null) { + throw new IllegalArgumentException("name cannot be null"); + } + if (callback == null) { + throw new IllegalArgumentException("callback cannot be null"); + } + if (handler == null) { + throw new IllegalArgumentException("handler cannot be null"); + } + SessionCreateCallbackRecord record = new SessionCreateCallbackRecord(callback, handler); + synchronized (mSessionCreateCallbackRecordMap) { + int seq = mNextSeq++; + mSessionCreateCallbackRecordMap.put(seq, record); + try { + mService.createSession(mClient, name, seq, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + } + + /** The Session provides the per-session functionality of TV inputs. */ + public static final class Session { + private final ITvInputManager mService; + private final IBinder mToken; + private final int mUserId; + + /** @hide */ + private Session(ComponentName name, IBinder token, ITvInputManager service, int userId) { + mToken = token; + mService = service; + mUserId = userId; + } + + /** + * Releases this session. + */ + public void release() { + try { + mService.releaseSession(mToken, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** + * Sets the {@link android.view.Surface} for this session. + * + * @param surface A {@link android.view.Surface} used to render video. + */ + public void setSurface(Surface surface) { + // surface can be null. + try { + mService.setSurface(mToken, surface, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** + * Sets the relative volume of this session to handle a change of audio focus. + * + * @param volume A volume value between 0.0f to 1.0f. + * @throws IllegalArgumentException if the volume value is out of range. + */ + public void setVolume(float volume) { + try { + if (volume < 0.0f || volume > 1.0f) { + throw new IllegalArgumentException("volume should be between 0.0f and 1.0f"); + } + mService.setVolume(mToken, volume, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** + * Tunes to a given channel. + * + * @param channelUri The URI of a channel. + * @throws IllegalArgumentException if the argument is {@code null}. + */ + public void tune(Uri channelUri) { + if (channelUri == null) { + throw new IllegalArgumentException("channelUri cannot be null"); + } + try { + mService.tune(mToken, channelUri, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/core/java/android/tv/TvInputService.java b/core/java/android/tv/TvInputService.java new file mode 100644 index 0000000..d7f6c32 --- /dev/null +++ b/core/java/android/tv/TvInputService.java @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2014 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.tv; + +import android.app.Service; +import android.content.ComponentName; +import android.content.Intent; +import android.net.Uri; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.util.Log; +import android.view.Surface; + +import com.android.internal.annotations.VisibleForTesting; + +/** + * A base class for implementing television input service. + */ +public abstract class TvInputService extends Service { + // STOPSHIP: Turn debugging off. + private static final boolean DEBUG = true; + private static final String TAG = "TvInputService"; + + /** + * This is the interface name that a service implementing a TV input should say that it support + * -- that is, this is the action it uses for its intent filter. To be supported, the service + * must also require the {@link android.Manifest.permission#BIND_TV_INPUT} permission so that + * other applications cannot abuse it. + */ + public static final String SERVICE_INTERFACE = "android.tv.TvInputService"; + + private ComponentName mComponentName; + private final Handler mHandler = new ServiceHandler(); + private final RemoteCallbackList<ITvInputServiceCallback> mCallbacks = + new RemoteCallbackList<ITvInputServiceCallback>(); + private boolean mAvailable; + + @Override + public void onCreate() { + super.onCreate(); + mComponentName = new ComponentName(getPackageName(), getClass().getName()); + } + + @Override + public final IBinder onBind(Intent intent) { + return new ITvInputService.Stub() { + @Override + public void registerCallback(ITvInputServiceCallback cb) { + if (cb != null) { + mCallbacks.register(cb); + // The first time a callback is registered, the service needs to report its + // availability status so that the system can know its initial value. + try { + cb.onAvailabilityChanged(mComponentName, mAvailable); + } catch (RemoteException e) { + Log.e(TAG, "error in onAvailabilityChanged", e); + } + } + } + + @Override + public void unregisterCallback(ITvInputServiceCallback cb) { + if (cb != null) { + mCallbacks.unregister(cb); + } + } + + @Override + public void createSession(ITvInputSessionCallback cb) { + if (cb != null) { + mHandler.obtainMessage(ServiceHandler.DO_CREATE_SESSION, cb).sendToTarget(); + } + } + }; + } + + /** + * Convenience method to notify an availability change of this TV input service. + * + * @param available {@code true} if the input service is available to show TV programs. + */ + public final void setAvailable(boolean available) { + if (available != mAvailable) { + mAvailable = available; + mHandler.obtainMessage(ServiceHandler.DO_BROADCAST_AVAILABILITY_CHANGE, available) + .sendToTarget(); + } + } + + /** + * Get the number of callbacks that are registered. + * + * @hide + */ + @VisibleForTesting + public final int getRegisteredCallbackCount() { + return mCallbacks.getRegisteredCallbackCount(); + } + + /** + * Returns a concrete implementation of {@link TvInputSessionImpl}. + * <p> + * May return {@code null} if this TV input service fails to create a session for some reason. + * </p> + */ + public abstract TvInputSessionImpl onCreateSession(); + + /** + * Base class for derived classes to implement to provide {@link TvInputManager.Session}. + */ + public abstract static class TvInputSessionImpl { + /** + * Called when the session is released. + */ + public abstract void onRelease(); + + /** + * Sets the {@link Surface} for the current input session on which the TV input renders + * video. + * + * @param surface {@link Surface} an application passes to this TV input session. + * @return {@code true} if the surface was set, {@code false} otherwise. + */ + public abstract boolean onSetSurface(Surface surface); + + /** + * Sets the relative volume of the current TV input session to handle the change of audio + * focus by setting. + * + * @param volume Volume scale from 0.0 to 1.0. + */ + public abstract void onSetVolume(float volume); + + /** + * Tunes to a given channel. + * + * @param channelUri The URI of the channel. + * @return {@code true} the tuning was successful, {@code false} otherwise. + */ + public abstract boolean onTune(Uri channelUri); + + /** + * This method is called when the application would like to stop using the current input + * session. + */ + void release() { + onRelease(); + } + + /** + * Calls {@link onSetSurface}. + */ + void setSurface(Surface surface) { + onSetSurface(surface); + // TODO: Handle failure. + } + + /** + * Calls {@link onSetVolume}. + */ + void setVolume(float volume) { + onSetVolume(volume); + } + + /** + * Calls {@link onTune}. + */ + void tune(Uri channelUri) { + onTune(channelUri); + // TODO: Handle failure. + } + } + + private final class ServiceHandler extends Handler { + private static final int DO_CREATE_SESSION = 1; + private static final int DO_BROADCAST_AVAILABILITY_CHANGE = 2; + + @Override + public final void handleMessage(Message msg) { + switch (msg.what) { + case DO_CREATE_SESSION: { + ITvInputSessionCallback cb = (ITvInputSessionCallback) msg.obj; + try { + TvInputSessionImpl sessionImpl = onCreateSession(); + if (sessionImpl == null) { + // Failed to create a session. + cb.onSessionCreated(null); + return; + } + ITvInputSession stub = new ITvInputSessionWrapper(TvInputService.this, + sessionImpl); + cb.onSessionCreated(stub); + } catch (RemoteException e) { + Log.e(TAG, "error in onSessionCreated"); + } + return; + } + case DO_BROADCAST_AVAILABILITY_CHANGE: { + boolean isAvailable = (Boolean) msg.obj; + int n = mCallbacks.beginBroadcast(); + try { + for (int i = 0; i < n; i++) { + mCallbacks.getBroadcastItem(i).onAvailabilityChanged(mComponentName, + isAvailable); + } + } catch (RemoteException e) { + Log.e(TAG, "Unexpected exception", e); + } finally { + mCallbacks.finishBroadcast(); + } + return; + } + default: { + Log.w(TAG, "Unhandled message code: " + msg.what); + return; + } + } + } + } +} diff --git a/core/java/android/util/ArrayMap.java b/core/java/android/util/ArrayMap.java index df1d4cd..9a0b7fc 100644 --- a/core/java/android/util/ArrayMap.java +++ b/core/java/android/util/ArrayMap.java @@ -16,6 +16,8 @@ package android.util; +import libcore.util.EmptyArray; + import java.util.Collection; import java.util.Map; import java.util.Set; @@ -234,8 +236,8 @@ public final class ArrayMap<K, V> implements Map<K, V> { * will grow once items are added to it. */ public ArrayMap() { - mHashes = ContainerHelpers.EMPTY_INTS; - mArray = ContainerHelpers.EMPTY_OBJECTS; + mHashes = EmptyArray.INT; + mArray = EmptyArray.OBJECT; mSize = 0; } @@ -244,8 +246,8 @@ public final class ArrayMap<K, V> implements Map<K, V> { */ public ArrayMap(int capacity) { if (capacity == 0) { - mHashes = ContainerHelpers.EMPTY_INTS; - mArray = ContainerHelpers.EMPTY_OBJECTS; + mHashes = EmptyArray.INT; + mArray = EmptyArray.OBJECT; } else { allocArrays(capacity); } @@ -253,8 +255,8 @@ public final class ArrayMap<K, V> implements Map<K, V> { } private ArrayMap(boolean immutable) { - mHashes = EMPTY_IMMUTABLE_INTS; - mArray = ContainerHelpers.EMPTY_OBJECTS; + mHashes = EmptyArray.INT; + mArray = EmptyArray.OBJECT; mSize = 0; } @@ -275,8 +277,8 @@ public final class ArrayMap<K, V> implements Map<K, V> { public void clear() { if (mSize > 0) { freeArrays(mHashes, mArray, mSize); - mHashes = ContainerHelpers.EMPTY_INTS; - mArray = ContainerHelpers.EMPTY_OBJECTS; + mHashes = EmptyArray.INT; + mArray = EmptyArray.OBJECT; mSize = 0; } } @@ -540,8 +542,8 @@ public final class ArrayMap<K, V> implements Map<K, V> { // Now empty. if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to 0"); freeArrays(mHashes, mArray, mSize); - mHashes = ContainerHelpers.EMPTY_INTS; - mArray = ContainerHelpers.EMPTY_OBJECTS; + mHashes = EmptyArray.INT; + mArray = EmptyArray.OBJECT; mSize = 0; } else { if (mHashes.length > (BASE_SIZE*2) && mSize < mHashes.length/3) { diff --git a/core/java/android/util/ArraySet.java b/core/java/android/util/ArraySet.java index 3c695e9..9d4b720 100644 --- a/core/java/android/util/ArraySet.java +++ b/core/java/android/util/ArraySet.java @@ -16,6 +16,8 @@ package android.util; +import libcore.util.EmptyArray; + import java.lang.reflect.Array; import java.util.Collection; import java.util.Iterator; @@ -222,8 +224,8 @@ public final class ArraySet<E> implements Collection<E>, Set<E> { * will grow once items are added to it. */ public ArraySet() { - mHashes = ContainerHelpers.EMPTY_INTS; - mArray = ContainerHelpers.EMPTY_OBJECTS; + mHashes = EmptyArray.INT; + mArray = EmptyArray.OBJECT; mSize = 0; } @@ -232,8 +234,8 @@ public final class ArraySet<E> implements Collection<E>, Set<E> { */ public ArraySet(int capacity) { if (capacity == 0) { - mHashes = ContainerHelpers.EMPTY_INTS; - mArray = ContainerHelpers.EMPTY_OBJECTS; + mHashes = EmptyArray.INT; + mArray = EmptyArray.OBJECT; } else { allocArrays(capacity); } @@ -258,8 +260,8 @@ public final class ArraySet<E> implements Collection<E>, Set<E> { public void clear() { if (mSize != 0) { freeArrays(mHashes, mArray, mSize); - mHashes = ContainerHelpers.EMPTY_INTS; - mArray = ContainerHelpers.EMPTY_OBJECTS; + mHashes = EmptyArray.INT; + mArray = EmptyArray.OBJECT; mSize = 0; } } @@ -413,8 +415,8 @@ public final class ArraySet<E> implements Collection<E>, Set<E> { // Now empty. if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to 0"); freeArrays(mHashes, mArray, mSize); - mHashes = ContainerHelpers.EMPTY_INTS; - mArray = ContainerHelpers.EMPTY_OBJECTS; + mHashes = EmptyArray.INT; + mArray = EmptyArray.OBJECT; mSize = 0; } else { if (mHashes.length > (BASE_SIZE*2) && mSize < mHashes.length/3) { diff --git a/core/java/android/util/ContainerHelpers.java b/core/java/android/util/ContainerHelpers.java index 624c4bd..4e5fefb 100644 --- a/core/java/android/util/ContainerHelpers.java +++ b/core/java/android/util/ContainerHelpers.java @@ -17,10 +17,6 @@ package android.util; class ContainerHelpers { - static final boolean[] EMPTY_BOOLEANS = new boolean[0]; - static final int[] EMPTY_INTS = new int[0]; - static final long[] EMPTY_LONGS = new long[0]; - static final Object[] EMPTY_OBJECTS = new Object[0]; // This is Arrays.binarySearch(), but doesn't do any argument validation. static int binarySearch(int[] array, int size, int value) { diff --git a/core/java/android/util/EventLogTags.java b/core/java/android/util/EventLogTags.java index 8c18417..f4ce4fd 100644 --- a/core/java/android/util/EventLogTags.java +++ b/core/java/android/util/EventLogTags.java @@ -16,14 +16,8 @@ package android.util; -import android.util.Log; - import java.io.BufferedReader; -import java.io.FileReader; import java.io.IOException; -import java.util.HashMap; -import java.util.regex.Matcher; -import java.util.regex.Pattern; /** * @deprecated This class is no longer functional. diff --git a/core/java/android/util/LocalLog.java b/core/java/android/util/LocalLog.java index 641d1b4..eeb6d58 100644 --- a/core/java/android/util/LocalLog.java +++ b/core/java/android/util/LocalLog.java @@ -20,7 +20,6 @@ import android.text.format.Time; import java.io.FileDescriptor; import java.io.PrintWriter; -import java.io.StringWriter; import java.util.Iterator; import java.util.LinkedList; diff --git a/core/java/android/util/LongArray.java b/core/java/android/util/LongArray.java new file mode 100644 index 0000000..54a6882 --- /dev/null +++ b/core/java/android/util/LongArray.java @@ -0,0 +1,166 @@ +/* + * 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.util; + +import com.android.internal.util.ArrayUtils; +import libcore.util.EmptyArray; + +/** + * Implements a growing array of long primitives. + * + * @hide + */ +public class LongArray implements Cloneable { + private static final int MIN_CAPACITY_INCREMENT = 12; + + private long[] mValues; + private int mSize; + + /** + * Creates an empty LongArray with the default initial capacity. + */ + public LongArray() { + this(10); + } + + /** + * Creates an empty LongArray with the specified initial capacity. + */ + public LongArray(int initialCapacity) { + if (initialCapacity == 0) { + mValues = EmptyArray.LONG; + } else { + mValues = ArrayUtils.newUnpaddedLongArray(initialCapacity); + } + mSize = 0; + } + + /** + * Appends the specified value to the end of this array. + */ + public void add(long value) { + add(mSize, value); + } + + /** + * Inserts a value at the specified position in this array. + * + * @throws IndexOutOfBoundsException when index < 0 || index > size() + */ + public void add(int index, long value) { + if (index < 0 || index > mSize) { + throw new IndexOutOfBoundsException(); + } + + ensureCapacity(1); + + if (mSize - index != 0) { + System.arraycopy(mValues, index, mValues, index + 1, mSize - index); + } + + mValues[index] = value; + mSize++; + } + + /** + * Adds the values in the specified array to this array. + */ + public void addAll(LongArray values) { + final int count = values.mSize; + ensureCapacity(count); + + System.arraycopy(values.mValues, 0, mValues, mSize, count); + mSize += count; + } + + /** + * Ensures capacity to append at least <code>count</code> values. + */ + private void ensureCapacity(int count) { + final int currentSize = mSize; + final int minCapacity = currentSize + count; + if (minCapacity >= mValues.length) { + final int targetCap = currentSize + (currentSize < (MIN_CAPACITY_INCREMENT / 2) ? + MIN_CAPACITY_INCREMENT : currentSize >> 1); + final int newCapacity = targetCap > minCapacity ? targetCap : minCapacity; + final long[] newValues = ArrayUtils.newUnpaddedLongArray(newCapacity); + System.arraycopy(mValues, 0, newValues, 0, currentSize); + mValues = newValues; + } + } + + /** + * Removes all values from this array. + */ + public void clear() { + mSize = 0; + } + + @Override + public LongArray clone() { + LongArray clone = null; + try { + clone = (LongArray) super.clone(); + clone.mValues = mValues.clone(); + } catch (CloneNotSupportedException cnse) { + /* ignore */ + } + return clone; + } + + /** + * Returns the value at the specified position in this array. + */ + public long get(int index) { + if (index >= mSize) { + throw new ArrayIndexOutOfBoundsException(mSize, index); + } + return mValues[index]; + } + + /** + * Returns the index of the first occurrence of the specified value in this + * array, or -1 if this array does not contain the value. + */ + public int indexOf(long value) { + final int n = mSize; + for (int i = 0; i < n; i++) { + if (mValues[i] == value) { + return i; + } + } + return -1; + } + + /** + * Removes the value at the specified index from this array. + */ + public void remove(int index) { + if (index >= mSize) { + throw new ArrayIndexOutOfBoundsException(mSize, index); + } + System.arraycopy(mValues, index + 1, mValues, index, mSize - index - 1); + mSize--; + } + + /** + * Returns the number of values in this array. + */ + public int size() { + return mSize; + } +} diff --git a/core/java/android/util/LongSparseArray.java b/core/java/android/util/LongSparseArray.java index dab853a..6b45ff4 100644 --- a/core/java/android/util/LongSparseArray.java +++ b/core/java/android/util/LongSparseArray.java @@ -17,6 +17,9 @@ package android.util; import com.android.internal.util.ArrayUtils; +import com.android.internal.util.GrowingArrayUtils; + +import libcore.util.EmptyArray; /** * SparseArray mapping longs to Objects. Unlike a normal array of Objects, @@ -70,12 +73,11 @@ public class LongSparseArray<E> implements Cloneable { */ public LongSparseArray(int initialCapacity) { if (initialCapacity == 0) { - mKeys = ContainerHelpers.EMPTY_LONGS; - mValues = ContainerHelpers.EMPTY_OBJECTS; + mKeys = EmptyArray.LONG; + mValues = EmptyArray.OBJECT; } else { - initialCapacity = ArrayUtils.idealLongArraySize(initialCapacity); - mKeys = new long[initialCapacity]; - mValues = new Object[initialCapacity]; + mKeys = ArrayUtils.newUnpaddedLongArray(initialCapacity); + mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity); } mSize = 0; } @@ -202,28 +204,8 @@ public class LongSparseArray<E> implements Cloneable { i = ~ContainerHelpers.binarySearch(mKeys, mSize, key); } - if (mSize >= mKeys.length) { - int n = ArrayUtils.idealLongArraySize(mSize + 1); - - long[] nkeys = new long[n]; - Object[] nvalues = new Object[n]; - - // Log.e("SparseArray", "grow " + mKeys.length + " to " + n); - System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length); - System.arraycopy(mValues, 0, nvalues, 0, mValues.length); - - mKeys = nkeys; - mValues = nvalues; - } - - if (mSize - i != 0) { - // Log.e("SparseArray", "move " + (mSize - i)); - System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i); - System.arraycopy(mValues, i, mValues, i + 1, mSize - i); - } - - mKeys[i] = key; - mValues[i] = value; + mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key); + mValues = GrowingArrayUtils.insert(mValues, mSize, i, value); mSize++; } } @@ -353,24 +335,9 @@ public class LongSparseArray<E> implements Cloneable { gc(); } - int pos = mSize; - if (pos >= mKeys.length) { - int n = ArrayUtils.idealLongArraySize(pos + 1); - - long[] nkeys = new long[n]; - Object[] nvalues = new Object[n]; - - // Log.e("SparseArray", "grow " + mKeys.length + " to " + n); - System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length); - System.arraycopy(mValues, 0, nvalues, 0, mValues.length); - - mKeys = nkeys; - mValues = nvalues; - } - - mKeys[pos] = key; - mValues[pos] = value; - mSize = pos + 1; + mKeys = GrowingArrayUtils.append(mKeys, mSize, key); + mValues = GrowingArrayUtils.append(mValues, mSize, value); + mSize++; } /** diff --git a/core/java/android/util/LongSparseLongArray.java b/core/java/android/util/LongSparseLongArray.java index 6654899..a361457 100644 --- a/core/java/android/util/LongSparseLongArray.java +++ b/core/java/android/util/LongSparseLongArray.java @@ -17,8 +17,9 @@ package android.util; import com.android.internal.util.ArrayUtils; +import com.android.internal.util.GrowingArrayUtils; -import java.util.Arrays; +import libcore.util.EmptyArray; /** * Map of {@code long} to {@code long}. Unlike a normal array of longs, there @@ -64,12 +65,11 @@ public class LongSparseLongArray implements Cloneable { */ public LongSparseLongArray(int initialCapacity) { if (initialCapacity == 0) { - mKeys = ContainerHelpers.EMPTY_LONGS; - mValues = ContainerHelpers.EMPTY_LONGS; + mKeys = EmptyArray.LONG; + mValues = EmptyArray.LONG; } else { - initialCapacity = ArrayUtils.idealLongArraySize(initialCapacity); - mKeys = new long[initialCapacity]; - mValues = new long[initialCapacity]; + mKeys = ArrayUtils.newUnpaddedLongArray(initialCapacity); + mValues = new long[mKeys.length]; } mSize = 0; } @@ -142,17 +142,8 @@ public class LongSparseLongArray implements Cloneable { } else { i = ~i; - if (mSize >= mKeys.length) { - growKeyAndValueArrays(mSize + 1); - } - - if (mSize - i != 0) { - System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i); - System.arraycopy(mValues, i, mValues, i + 1, mSize - i); - } - - mKeys[i] = key; - mValues[i] = value; + mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key); + mValues = GrowingArrayUtils.insert(mValues, mSize, i, value); mSize++; } } @@ -236,27 +227,9 @@ public class LongSparseLongArray implements Cloneable { return; } - int pos = mSize; - if (pos >= mKeys.length) { - growKeyAndValueArrays(pos + 1); - } - - mKeys[pos] = key; - mValues[pos] = value; - mSize = pos + 1; - } - - private void growKeyAndValueArrays(int minNeededSize) { - int n = ArrayUtils.idealLongArraySize(minNeededSize); - - long[] nkeys = new long[n]; - long[] nvalues = new long[n]; - - System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length); - System.arraycopy(mValues, 0, nvalues, 0, mValues.length); - - mKeys = nkeys; - mValues = nvalues; + mKeys = GrowingArrayUtils.append(mKeys, mSize, key); + mValues = GrowingArrayUtils.append(mValues, mSize, value); + mSize++; } /** diff --git a/core/java/android/util/LruCache.java b/core/java/android/util/LruCache.java index dd504c1..4015488 100644 --- a/core/java/android/util/LruCache.java +++ b/core/java/android/util/LruCache.java @@ -87,9 +87,8 @@ public class LruCache<K, V> { /** * Sets the size of the cache. - * @param maxSize The new maximum size. * - * @hide + * @param maxSize The new maximum size. */ public void resize(int maxSize) { if (maxSize <= 0) { diff --git a/core/java/android/util/Patterns.java b/core/java/android/util/Patterns.java index 0f8da44..13cc88b 100644 --- a/core/java/android/util/Patterns.java +++ b/core/java/android/util/Patterns.java @@ -28,7 +28,12 @@ public class Patterns { * List accurate as of 2011/07/18. List taken from: * http://data.iana.org/TLD/tlds-alpha-by-domain.txt * This pattern is auto-generated by frameworks/ex/common/tools/make-iana-tld-pattern.py + * + * @deprecated Due to the recent profileration of gTLDs, this API is + * expected to become out-of-date very quickly. Therefore it is now + * deprecated. */ + @Deprecated public static final String TOP_LEVEL_DOMAIN_STR = "((aero|arpa|asia|a[cdefgilmnoqrstuwxz])" + "|(biz|b[abdefghijmnorstvwyz])" @@ -59,7 +64,9 @@ public class Patterns { /** * Regular expression pattern to match all IANA top-level domains. + * @deprecated This API is deprecated. See {@link #TOP_LEVEL_DOMAIN_STR}. */ + @Deprecated public static final Pattern TOP_LEVEL_DOMAIN = Pattern.compile(TOP_LEVEL_DOMAIN_STR); @@ -68,7 +75,10 @@ public class Patterns { * List accurate as of 2011/07/18. List taken from: * http://data.iana.org/TLD/tlds-alpha-by-domain.txt * This pattern is auto-generated by frameworks/ex/common/tools/make-iana-tld-pattern.py + * + * @deprecated This API is deprecated. See {@link #TOP_LEVEL_DOMAIN_STR}. */ + @Deprecated public static final String TOP_LEVEL_DOMAIN_STR_FOR_WEB_URL = "(?:" + "(?:aero|arpa|asia|a[cdefgilmnoqrstuwxz])" @@ -107,6 +117,24 @@ public class Patterns { public static final String GOOD_IRI_CHAR = "a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF"; + public static final Pattern IP_ADDRESS + = Pattern.compile( + "((25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(25[0-5]|2[0-4]" + + "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]" + + "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}" + + "|[1-9][0-9]|[0-9]))"); + + /** + * RFC 1035 Section 2.3.4 limits the labels to a maximum 63 octets. + */ + private static final String IRI + = "[" + GOOD_IRI_CHAR + "]([" + GOOD_IRI_CHAR + "\\-]{0,61}[" + GOOD_IRI_CHAR + "]){0,1}"; + + private static final String HOST_NAME = IRI + "(?:\\." + IRI + ")+"; + + public static final Pattern DOMAIN_NAME + = Pattern.compile("(" + HOST_NAME + "|" + IP_ADDRESS + ")"); + /** * Regular expression pattern to match most part of RFC 3987 * Internationalized URLs, aka IRIs. Commonly used Unicode characters are @@ -116,13 +144,7 @@ public class Patterns { "((?:(http|https|Http|Https|rtsp|Rtsp):\\/\\/(?:(?:[a-zA-Z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)" + "\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,64}(?:\\:(?:[a-zA-Z0-9\\$\\-\\_" + "\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,25})?\\@)?)?" - + "((?:(?:[" + GOOD_IRI_CHAR + "][" + GOOD_IRI_CHAR + "\\-]{0,64}\\.)+" // named host - + TOP_LEVEL_DOMAIN_STR_FOR_WEB_URL - + "|(?:(?:25[0-5]|2[0-4]" // or ip address - + "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(?:25[0-5]|2[0-4][0-9]" - + "|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(?:25[0-5]|2[0-4][0-9]|[0-1]" - + "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(?:25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}" - + "|[1-9][0-9]|[0-9])))" + + "(?:" + DOMAIN_NAME + ")" + "(?:\\:\\d{1,5})?)" // plus option port number + "(\\/(?:(?:[" + GOOD_IRI_CHAR + "\\;\\/\\?\\:\\@\\&\\=\\#\\~" // plus option query params + "\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])|(?:\\%[a-fA-F0-9]{2}))*)?" @@ -130,19 +152,6 @@ public class Patterns { // input. This is to stop foo.sure from // matching as foo.su - public static final Pattern IP_ADDRESS - = Pattern.compile( - "((25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(25[0-5]|2[0-4]" - + "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]" - + "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}" - + "|[1-9][0-9]|[0-9]))"); - - public static final Pattern DOMAIN_NAME - = Pattern.compile( - "(((([" + GOOD_IRI_CHAR + "][" + GOOD_IRI_CHAR + "\\-]*)*[" + GOOD_IRI_CHAR + "]\\.)+" - + TOP_LEVEL_DOMAIN + ")|" - + IP_ADDRESS + ")"); - public static final Pattern EMAIL_ADDRESS = Pattern.compile( "[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" + @@ -159,7 +168,7 @@ public class Patterns { * might be phone numbers in arbitrary text, not for validating whether * something is in fact a phone number. It will miss many things that * are legitimate phone numbers. - * + * * <p> The pattern matches the following: * <ul> * <li>Optionally, a + sign followed immediately by one or more digits. Spaces, dots, or dashes diff --git a/core/java/android/util/Slog.java b/core/java/android/util/Slog.java index 70795bb..b25d80f 100644 --- a/core/java/android/util/Slog.java +++ b/core/java/android/util/Slog.java @@ -16,11 +16,6 @@ package android.util; -import com.android.internal.os.RuntimeInit; - -import java.io.PrintWriter; -import java.io.StringWriter; - /** * @hide */ diff --git a/core/java/android/util/SparseArray.java b/core/java/android/util/SparseArray.java index 46d9d45..92e874f 100644 --- a/core/java/android/util/SparseArray.java +++ b/core/java/android/util/SparseArray.java @@ -17,6 +17,9 @@ package android.util; import com.android.internal.util.ArrayUtils; +import com.android.internal.util.GrowingArrayUtils; + +import libcore.util.EmptyArray; /** * SparseArrays map integers to Objects. Unlike a normal array of Objects, @@ -70,12 +73,11 @@ public class SparseArray<E> implements Cloneable { */ public SparseArray(int initialCapacity) { if (initialCapacity == 0) { - mKeys = ContainerHelpers.EMPTY_INTS; - mValues = ContainerHelpers.EMPTY_OBJECTS; + mKeys = EmptyArray.INT; + mValues = EmptyArray.OBJECT; } else { - initialCapacity = ArrayUtils.idealIntArraySize(initialCapacity); - mKeys = new int[initialCapacity]; - mValues = new Object[initialCapacity]; + mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity); + mKeys = new int[mValues.length]; } mSize = 0; } @@ -215,28 +217,8 @@ public class SparseArray<E> implements Cloneable { i = ~ContainerHelpers.binarySearch(mKeys, mSize, key); } - if (mSize >= mKeys.length) { - int n = ArrayUtils.idealIntArraySize(mSize + 1); - - int[] nkeys = new int[n]; - Object[] nvalues = new Object[n]; - - // Log.e("SparseArray", "grow " + mKeys.length + " to " + n); - System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length); - System.arraycopy(mValues, 0, nvalues, 0, mValues.length); - - mKeys = nkeys; - mValues = nvalues; - } - - if (mSize - i != 0) { - // Log.e("SparseArray", "move " + (mSize - i)); - System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i); - System.arraycopy(mValues, i, mValues, i + 1, mSize - i); - } - - mKeys[i] = key; - mValues[i] = value; + mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key); + mValues = GrowingArrayUtils.insert(mValues, mSize, i, value); mSize++; } } @@ -368,24 +350,9 @@ public class SparseArray<E> implements Cloneable { gc(); } - int pos = mSize; - if (pos >= mKeys.length) { - int n = ArrayUtils.idealIntArraySize(pos + 1); - - int[] nkeys = new int[n]; - Object[] nvalues = new Object[n]; - - // Log.e("SparseArray", "grow " + mKeys.length + " to " + n); - System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length); - System.arraycopy(mValues, 0, nvalues, 0, mValues.length); - - mKeys = nkeys; - mValues = nvalues; - } - - mKeys[pos] = key; - mValues[pos] = value; - mSize = pos + 1; + mKeys = GrowingArrayUtils.append(mKeys, mSize, key); + mValues = GrowingArrayUtils.append(mValues, mSize, value); + mSize++; } /** diff --git a/core/java/android/util/SparseBooleanArray.java b/core/java/android/util/SparseBooleanArray.java index 905dcb0..e293b1f 100644 --- a/core/java/android/util/SparseBooleanArray.java +++ b/core/java/android/util/SparseBooleanArray.java @@ -17,6 +17,9 @@ package android.util; import com.android.internal.util.ArrayUtils; +import com.android.internal.util.GrowingArrayUtils; + +import libcore.util.EmptyArray; /** * SparseBooleanArrays map integers to booleans. @@ -57,12 +60,11 @@ public class SparseBooleanArray implements Cloneable { */ public SparseBooleanArray(int initialCapacity) { if (initialCapacity == 0) { - mKeys = ContainerHelpers.EMPTY_INTS; - mValues = ContainerHelpers.EMPTY_BOOLEANS; + mKeys = EmptyArray.INT; + mValues = EmptyArray.BOOLEAN; } else { - initialCapacity = ArrayUtils.idealIntArraySize(initialCapacity); - mKeys = new int[initialCapacity]; - mValues = new boolean[initialCapacity]; + mKeys = ArrayUtils.newUnpaddedIntArray(initialCapacity); + mValues = new boolean[mKeys.length]; } mSize = 0; } @@ -115,6 +117,13 @@ public class SparseBooleanArray implements Cloneable { } } + /** @hide */ + public void removeAt(int index) { + System.arraycopy(mKeys, index + 1, mKeys, index, mSize - (index + 1)); + System.arraycopy(mValues, index + 1, mValues, index, mSize - (index + 1)); + mSize--; + } + /** * Adds a mapping from the specified key to the specified value, * replacing the previous mapping from the specified key if there @@ -128,28 +137,8 @@ public class SparseBooleanArray implements Cloneable { } else { i = ~i; - if (mSize >= mKeys.length) { - int n = ArrayUtils.idealIntArraySize(mSize + 1); - - int[] nkeys = new int[n]; - boolean[] nvalues = new boolean[n]; - - // Log.e("SparseBooleanArray", "grow " + mKeys.length + " to " + n); - System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length); - System.arraycopy(mValues, 0, nvalues, 0, mValues.length); - - mKeys = nkeys; - mValues = nvalues; - } - - if (mSize - i != 0) { - // Log.e("SparseBooleanArray", "move " + (mSize - i)); - System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i); - System.arraycopy(mValues, i, mValues, i + 1, mSize - i); - } - - mKeys[i] = key; - mValues[i] = value; + mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key); + mValues = GrowingArrayUtils.insert(mValues, mSize, i, value); mSize++; } } @@ -191,6 +180,11 @@ public class SparseBooleanArray implements Cloneable { return mValues[index]; } + /** @hide */ + public void setValueAt(int index, boolean value) { + mValues[index] = value; + } + /** * Returns the index for which {@link #keyAt} would return the * specified key, or a negative number if the specified @@ -233,24 +227,9 @@ public class SparseBooleanArray implements Cloneable { return; } - int pos = mSize; - if (pos >= mKeys.length) { - int n = ArrayUtils.idealIntArraySize(pos + 1); - - int[] nkeys = new int[n]; - boolean[] nvalues = new boolean[n]; - - // Log.e("SparseBooleanArray", "grow " + mKeys.length + " to " + n); - System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length); - System.arraycopy(mValues, 0, nvalues, 0, mValues.length); - - mKeys = nkeys; - mValues = nvalues; - } - - mKeys[pos] = key; - mValues[pos] = value; - mSize = pos + 1; + mKeys = GrowingArrayUtils.append(mKeys, mSize, key); + mValues = GrowingArrayUtils.append(mValues, mSize, value); + mSize++; } /** diff --git a/core/java/android/util/SparseIntArray.java b/core/java/android/util/SparseIntArray.java index 4f5ca07..2b85a21 100644 --- a/core/java/android/util/SparseIntArray.java +++ b/core/java/android/util/SparseIntArray.java @@ -17,6 +17,9 @@ package android.util; import com.android.internal.util.ArrayUtils; +import com.android.internal.util.GrowingArrayUtils; + +import libcore.util.EmptyArray; /** * SparseIntArrays map integers to integers. Unlike a normal array of integers, @@ -60,12 +63,11 @@ public class SparseIntArray implements Cloneable { */ public SparseIntArray(int initialCapacity) { if (initialCapacity == 0) { - mKeys = ContainerHelpers.EMPTY_INTS; - mValues = ContainerHelpers.EMPTY_INTS; + mKeys = EmptyArray.INT; + mValues = EmptyArray.INT; } else { - initialCapacity = ArrayUtils.idealIntArraySize(initialCapacity); - mKeys = new int[initialCapacity]; - mValues = new int[initialCapacity]; + mKeys = ArrayUtils.newUnpaddedIntArray(initialCapacity); + mValues = new int[mKeys.length]; } mSize = 0; } @@ -138,28 +140,8 @@ public class SparseIntArray implements Cloneable { } else { i = ~i; - if (mSize >= mKeys.length) { - int n = ArrayUtils.idealIntArraySize(mSize + 1); - - int[] nkeys = new int[n]; - int[] nvalues = new int[n]; - - // Log.e("SparseIntArray", "grow " + mKeys.length + " to " + n); - System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length); - System.arraycopy(mValues, 0, nvalues, 0, mValues.length); - - mKeys = nkeys; - mValues = nvalues; - } - - if (mSize - i != 0) { - // Log.e("SparseIntArray", "move " + (mSize - i)); - System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i); - System.arraycopy(mValues, i, mValues, i + 1, mSize - i); - } - - mKeys[i] = key; - mValues[i] = value; + mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key); + mValues = GrowingArrayUtils.insert(mValues, mSize, i, value); mSize++; } } @@ -243,24 +225,9 @@ public class SparseIntArray implements Cloneable { return; } - int pos = mSize; - if (pos >= mKeys.length) { - int n = ArrayUtils.idealIntArraySize(pos + 1); - - int[] nkeys = new int[n]; - int[] nvalues = new int[n]; - - // Log.e("SparseIntArray", "grow " + mKeys.length + " to " + n); - System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length); - System.arraycopy(mValues, 0, nvalues, 0, mValues.length); - - mKeys = nkeys; - mValues = nvalues; - } - - mKeys[pos] = key; - mValues[pos] = value; - mSize = pos + 1; + mKeys = GrowingArrayUtils.append(mKeys, mSize, key); + mValues = GrowingArrayUtils.append(mValues, mSize, value); + mSize++; } /** diff --git a/core/java/android/util/SparseLongArray.java b/core/java/android/util/SparseLongArray.java index 39fc8a3..0166c4a 100644 --- a/core/java/android/util/SparseLongArray.java +++ b/core/java/android/util/SparseLongArray.java @@ -17,6 +17,9 @@ package android.util; import com.android.internal.util.ArrayUtils; +import com.android.internal.util.GrowingArrayUtils; + +import libcore.util.EmptyArray; /** * SparseLongArrays map integers to longs. Unlike a normal array of longs, @@ -60,12 +63,11 @@ public class SparseLongArray implements Cloneable { */ public SparseLongArray(int initialCapacity) { if (initialCapacity == 0) { - mKeys = ContainerHelpers.EMPTY_INTS; - mValues = ContainerHelpers.EMPTY_LONGS; + mKeys = EmptyArray.INT; + mValues = EmptyArray.LONG; } else { - initialCapacity = ArrayUtils.idealLongArraySize(initialCapacity); - mKeys = new int[initialCapacity]; - mValues = new long[initialCapacity]; + mValues = ArrayUtils.newUnpaddedLongArray(initialCapacity); + mKeys = new int[mValues.length]; } mSize = 0; } @@ -138,17 +140,8 @@ public class SparseLongArray implements Cloneable { } else { i = ~i; - if (mSize >= mKeys.length) { - growKeyAndValueArrays(mSize + 1); - } - - if (mSize - i != 0) { - System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i); - System.arraycopy(mValues, i, mValues, i + 1, mSize - i); - } - - mKeys[i] = key; - mValues[i] = value; + mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key); + mValues = GrowingArrayUtils.insert(mValues, mSize, i, value); mSize++; } } @@ -232,27 +225,9 @@ public class SparseLongArray implements Cloneable { return; } - int pos = mSize; - if (pos >= mKeys.length) { - growKeyAndValueArrays(pos + 1); - } - - mKeys[pos] = key; - mValues[pos] = value; - mSize = pos + 1; - } - - private void growKeyAndValueArrays(int minNeededSize) { - int n = ArrayUtils.idealLongArraySize(minNeededSize); - - int[] nkeys = new int[n]; - long[] nvalues = new long[n]; - - System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length); - System.arraycopy(mValues, 0, nvalues, 0, mValues.length); - - mKeys = nkeys; - mValues = nvalues; + mKeys = GrowingArrayUtils.append(mKeys, mSize, key); + mValues = GrowingArrayUtils.append(mValues, mSize, value); + mSize++; } /** diff --git a/core/java/android/view/AccessibilityInteractionController.java b/core/java/android/view/AccessibilityInteractionController.java index 41d3700..477c994 100644 --- a/core/java/android/view/AccessibilityInteractionController.java +++ b/core/java/android/view/AccessibilityInteractionController.java @@ -18,13 +18,14 @@ package android.view; import android.graphics.Point; import android.graphics.Rect; +import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.Process; import android.os.RemoteException; -import android.util.SparseLongArray; +import android.util.LongSparseArray; import android.view.View.AttachInfo; import android.view.accessibility.AccessibilityInteractionClient; import android.view.accessibility.AccessibilityNodeInfo; @@ -36,8 +37,11 @@ import com.android.internal.util.Predicate; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Queue; /** * Class for managing accessibility interactions initiated from the system @@ -48,6 +52,8 @@ import java.util.Map; */ final class AccessibilityInteractionController { + private static final boolean ENFORCE_NODE_TREE_CONSISTENT = false; + private final ArrayList<AccessibilityNodeInfo> mTempAccessibilityNodeInfoList = new ArrayList<AccessibilityNodeInfo>(); @@ -138,7 +144,7 @@ final class AccessibilityInteractionController { } mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = flags; View root = null; - if (accessibilityViewId == AccessibilityNodeInfo.UNDEFINED) { + if (accessibilityViewId == AccessibilityNodeInfo.UNDEFINED_ITEM_ID) { root = mViewRootImpl.mView; } else { root = findViewByAccessibilityId(accessibilityViewId); @@ -210,7 +216,7 @@ final class AccessibilityInteractionController { } mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = flags; View root = null; - if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED) { + if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) { root = findViewByAccessibilityId(accessibilityViewId); } else { root = mViewRootImpl.mView; @@ -290,7 +296,7 @@ final class AccessibilityInteractionController { } mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = flags; View root = null; - if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED) { + if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) { root = findViewByAccessibilityId(accessibilityViewId); } else { root = mViewRootImpl.mView; @@ -298,9 +304,14 @@ final class AccessibilityInteractionController { if (root != null && isShown(root)) { AccessibilityNodeProvider provider = root.getAccessibilityNodeProvider(); if (provider != null) { - infos = provider.findAccessibilityNodeInfosByText(text, - virtualDescendantId); - } else if (virtualDescendantId == AccessibilityNodeInfo.UNDEFINED) { + if (virtualDescendantId != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) { + infos = provider.findAccessibilityNodeInfosByText(text, + virtualDescendantId); + } else { + infos = provider.findAccessibilityNodeInfosByText(text, + AccessibilityNodeProvider.HOST_VIEW_ID); + } + } else if (virtualDescendantId == AccessibilityNodeInfo.UNDEFINED_ITEM_ID) { ArrayList<View> foundViews = mTempArrayList; foundViews.clear(); root.findViewsWithText(foundViews, text, View.FIND_VIEWS_WITH_TEXT @@ -317,7 +328,7 @@ final class AccessibilityInteractionController { if (provider != null) { List<AccessibilityNodeInfo> infosFromProvider = provider.findAccessibilityNodeInfosByText(text, - AccessibilityNodeInfo.UNDEFINED); + AccessibilityNodeProvider.HOST_VIEW_ID); if (infosFromProvider != null) { infos.addAll(infosFromProvider); } @@ -392,7 +403,7 @@ final class AccessibilityInteractionController { } mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = flags; View root = null; - if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED) { + if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) { root = findViewByAccessibilityId(accessibilityViewId); } else { root = mViewRootImpl.mView; @@ -418,7 +429,7 @@ final class AccessibilityInteractionController { focused = AccessibilityNodeInfo.obtain( mViewRootImpl.mAccessibilityFocusedVirtualView); } - } else if (virtualDescendantId == View.NO_ID) { + } else if (virtualDescendantId == AccessibilityNodeInfo.UNDEFINED_ITEM_ID) { focused = host.createAccessibilityNodeInfo(); } } break; @@ -501,7 +512,7 @@ final class AccessibilityInteractionController { } mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = flags; View root = null; - if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED) { + if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) { root = findViewByAccessibilityId(accessibilityViewId); } else { root = mViewRootImpl.mView; @@ -577,7 +588,7 @@ final class AccessibilityInteractionController { } mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = flags; View target = null; - if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED) { + if (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) { target = findViewByAccessibilityId(accessibilityViewId); } else { target = mViewRootImpl.mView; @@ -585,9 +596,14 @@ final class AccessibilityInteractionController { if (target != null && isShown(target)) { AccessibilityNodeProvider provider = target.getAccessibilityNodeProvider(); if (provider != null) { - succeeded = provider.performAction(virtualDescendantId, action, - arguments); - } else if (virtualDescendantId == View.NO_ID) { + if (virtualDescendantId != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) { + succeeded = provider.performAction(virtualDescendantId, action, + arguments); + } else { + succeeded = provider.performAction(AccessibilityNodeProvider.HOST_VIEW_ID, + action, arguments); + } + } else if (virtualDescendantId == AccessibilityNodeInfo.UNDEFINED_ITEM_ID) { succeeded = target.performAccessibilityAction(action, arguments); } } @@ -735,6 +751,85 @@ final class AccessibilityInteractionController { } } } + if (ENFORCE_NODE_TREE_CONSISTENT) { + enforceNodeTreeConsistent(outInfos); + } + } + + private void enforceNodeTreeConsistent(List<AccessibilityNodeInfo> nodes) { + LongSparseArray<AccessibilityNodeInfo> nodeMap = + new LongSparseArray<AccessibilityNodeInfo>(); + final int nodeCount = nodes.size(); + for (int i = 0; i < nodeCount; i++) { + AccessibilityNodeInfo node = nodes.get(i); + nodeMap.put(node.getSourceNodeId(), node); + } + + // If the nodes are a tree it does not matter from + // which node we start to search for the root. + AccessibilityNodeInfo root = nodeMap.valueAt(0); + AccessibilityNodeInfo parent = root; + while (parent != null) { + root = parent; + parent = nodeMap.get(parent.getParentNodeId()); + } + + // Traverse the tree and do some checks. + AccessibilityNodeInfo accessFocus = null; + AccessibilityNodeInfo inputFocus = null; + HashSet<AccessibilityNodeInfo> seen = new HashSet<AccessibilityNodeInfo>(); + Queue<AccessibilityNodeInfo> fringe = new LinkedList<AccessibilityNodeInfo>(); + fringe.add(root); + + while (!fringe.isEmpty()) { + AccessibilityNodeInfo current = fringe.poll(); + + // Check for duplicates + if (!seen.add(current)) { + throw new IllegalStateException("Duplicate node: " + + current + " in window:" + + mViewRootImpl.mAttachInfo.mAccessibilityWindowId); + } + + // Check for one accessibility focus. + if (current.isAccessibilityFocused()) { + if (accessFocus != null) { + throw new IllegalStateException("Duplicate accessibility focus:" + + current + + " in window:" + mViewRootImpl.mAttachInfo.mAccessibilityWindowId); + } else { + accessFocus = current; + } + } + + // Check for one input focus. + if (current.isFocused()) { + if (inputFocus != null) { + throw new IllegalStateException("Duplicate input focus: " + + current + " in window:" + + mViewRootImpl.mAttachInfo.mAccessibilityWindowId); + } else { + inputFocus = current; + } + } + + final int childCount = current.getChildCount(); + for (int j = 0; j < childCount; j++) { + final long childId = current.getChildId(j); + final AccessibilityNodeInfo child = nodeMap.get(childId); + if (child != null) { + fringe.add(child); + } + } + } + + // Check for disconnected nodes. + for (int j = nodeMap.size() - 1; j >= 0; j--) { + AccessibilityNodeInfo info = nodeMap.valueAt(j); + if (!seen.contains(info)) { + throw new IllegalStateException("Disconnected node: " + info); + } + } } private void prefetchPredecessorsOfRealNode(View view, @@ -775,7 +870,7 @@ final class AccessibilityInteractionController { info = child.createAccessibilityNodeInfo(); } else { info = provider.createAccessibilityNodeInfo( - AccessibilityNodeInfo.UNDEFINED); + AccessibilityNodeProvider.HOST_VIEW_ID); } if (info != null) { outInfos.add(info); @@ -815,7 +910,7 @@ final class AccessibilityInteractionController { } } else { AccessibilityNodeInfo info = provider.createAccessibilityNodeInfo( - AccessibilityNodeInfo.UNDEFINED); + AccessibilityNodeProvider.HOST_VIEW_ID); if (info != null) { outInfos.add(info); addedChildren.put(child, info); @@ -846,16 +941,22 @@ final class AccessibilityInteractionController { List<AccessibilityNodeInfo> outInfos) { long parentNodeId = root.getParentNodeId(); int accessibilityViewId = AccessibilityNodeInfo.getAccessibilityViewId(parentNodeId); - while (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED) { + while (accessibilityViewId != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) { if (outInfos.size() >= MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) { return; } final int virtualDescendantId = AccessibilityNodeInfo.getVirtualDescendantId(parentNodeId); - if (virtualDescendantId != AccessibilityNodeInfo.UNDEFINED + if (virtualDescendantId != AccessibilityNodeInfo.UNDEFINED_ITEM_ID || accessibilityViewId == providerHost.getAccessibilityViewId()) { - AccessibilityNodeInfo parent = provider.createAccessibilityNodeInfo( - virtualDescendantId); + final AccessibilityNodeInfo parent; + if (virtualDescendantId != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) { + parent = provider.createAccessibilityNodeInfo( + virtualDescendantId); + } else { + parent= provider.createAccessibilityNodeInfo( + AccessibilityNodeProvider.HOST_VIEW_ID); + } if (parent != null) { outInfos.add(parent); } @@ -876,18 +977,22 @@ final class AccessibilityInteractionController { AccessibilityNodeInfo.getAccessibilityViewId(parentNodeId); final int parentVirtualDescendantId = AccessibilityNodeInfo.getVirtualDescendantId(parentNodeId); - if (parentVirtualDescendantId != AccessibilityNodeInfo.UNDEFINED + if (parentVirtualDescendantId != AccessibilityNodeInfo.UNDEFINED_ITEM_ID || parentAccessibilityViewId == providerHost.getAccessibilityViewId()) { - AccessibilityNodeInfo parent = - provider.createAccessibilityNodeInfo(parentVirtualDescendantId); + final AccessibilityNodeInfo parent; + if (parentAccessibilityViewId != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) { + parent = provider.createAccessibilityNodeInfo(parentVirtualDescendantId); + } else { + parent = provider.createAccessibilityNodeInfo( + AccessibilityNodeProvider.HOST_VIEW_ID); + } if (parent != null) { - SparseLongArray childNodeIds = parent.getChildNodeIds(); - final int childCount = childNodeIds.size(); + final int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { if (outInfos.size() >= MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) { return; } - final long childNodeId = childNodeIds.get(i); + final long childNodeId = parent.getChildId(i); if (childNodeId != current.getSourceNodeId()) { final int childVirtualDescendantId = AccessibilityNodeInfo.getVirtualDescendantId(childNodeId); @@ -906,14 +1011,13 @@ final class AccessibilityInteractionController { private void prefetchDescendantsOfVirtualNode(AccessibilityNodeInfo root, AccessibilityNodeProvider provider, List<AccessibilityNodeInfo> outInfos) { - SparseLongArray childNodeIds = root.getChildNodeIds(); final int initialOutInfosSize = outInfos.size(); - final int childCount = childNodeIds.size(); + final int childCount = root.getChildCount(); for (int i = 0; i < childCount; i++) { if (outInfos.size() >= MAX_ACCESSIBILITY_NODE_INFO_BATCH_SIZE) { return; } - final long childNodeId = childNodeIds.get(i); + final long childNodeId = root.getChildId(i); AccessibilityNodeInfo child = provider.createAccessibilityNodeInfo( AccessibilityNodeInfo.getVirtualDescendantId(childNodeId)); if (child != null) { diff --git a/core/java/android/view/AccessibilityIterators.java b/core/java/android/view/AccessibilityIterators.java index 17ce4f6..e59937d 100644 --- a/core/java/android/view/AccessibilityIterators.java +++ b/core/java/android/view/AccessibilityIterators.java @@ -17,8 +17,6 @@ package android.view; import android.content.ComponentCallbacks; -import android.content.Context; -import android.content.pm.ActivityInfo; import android.content.res.Configuration; import java.text.BreakIterator; diff --git a/core/java/android/view/AnimationRenderStats.aidl b/core/java/android/view/AnimationRenderStats.aidl new file mode 100644 index 0000000..4599708 --- /dev/null +++ b/core/java/android/view/AnimationRenderStats.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2014, 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.view; + +parcelable AnimationRenderStats; diff --git a/core/java/android/view/ContextThemeWrapper.java b/core/java/android/view/ContextThemeWrapper.java index 6c733f9..0afbde9 100644 --- a/core/java/android/view/ContextThemeWrapper.java +++ b/core/java/android/view/ContextThemeWrapper.java @@ -20,14 +20,12 @@ import android.content.Context; import android.content.ContextWrapper; import android.content.res.Configuration; import android.content.res.Resources; -import android.os.Build; /** * A ContextWrapper that allows you to modify the theme from what is in the * wrapped context. */ public class ContextThemeWrapper extends ContextWrapper { - private Context mBase; private int mThemeResource; private Resources.Theme mTheme; private LayoutInflater mInflater; @@ -40,13 +38,11 @@ public class ContextThemeWrapper extends ContextWrapper { public ContextThemeWrapper(Context base, int themeres) { super(base); - mBase = base; mThemeResource = themeres; } @Override protected void attachBaseContext(Context newBase) { super.attachBaseContext(newBase); - mBase = newBase; } /** @@ -110,11 +106,11 @@ public class ContextThemeWrapper extends ContextWrapper { @Override public Object getSystemService(String name) { if (LAYOUT_INFLATER_SERVICE.equals(name)) { if (mInflater == null) { - mInflater = LayoutInflater.from(mBase).cloneInContext(this); + mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this); } return mInflater; } - return mBase.getSystemService(name); + return getBaseContext().getSystemService(name); } /** @@ -136,7 +132,7 @@ public class ContextThemeWrapper extends ContextWrapper { final boolean first = mTheme == null; if (first) { mTheme = getResources().newTheme(); - Resources.Theme theme = mBase.getTheme(); + Resources.Theme theme = getBaseContext().getTheme(); if (theme != null) { mTheme.setTo(theme); } diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java index c4494f4..d7a913d 100644 --- a/core/java/android/view/Display.java +++ b/core/java/android/view/Display.java @@ -558,6 +558,7 @@ public final class Display { * 90 degrees clockwise and thus the returned value here will be * {@link Surface#ROTATION_90 Surface.ROTATION_90}. */ + @Surface.Rotation public int getRotation() { synchronized (this) { updateDisplayInfoLocked(); @@ -570,6 +571,7 @@ public final class Display { * @return orientation of this display. */ @Deprecated + @Surface.Rotation public int getOrientation() { return getRotation(); } diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java index 5f840d3..b0fe0fa 100644 --- a/core/java/android/view/DisplayInfo.java +++ b/core/java/android/view/DisplayInfo.java @@ -20,7 +20,6 @@ import android.content.res.CompatibilityInfo; import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; -import android.os.Process; import android.util.DisplayMetrics; import libcore.util.Objects; @@ -144,6 +143,7 @@ public final class DisplayInfo implements Parcelable { * more than one physical display. * </p> */ + @Surface.Rotation public int rotation; /** diff --git a/core/java/android/view/DisplayList.java b/core/java/android/view/DisplayList.java deleted file mode 100644 index 43fd628..0000000 --- a/core/java/android/view/DisplayList.java +++ /dev/null @@ -1,686 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.view; - -import android.graphics.Matrix; - -/** - * <p>A display list records a series of graphics related operations and can replay - * them later. Display lists are usually built by recording operations on a - * {@link HardwareCanvas}. Replaying the operations from a display list avoids - * executing application code on every frame, and is thus much more efficient.</p> - * - * <p>Display lists are used internally for all views by default, and are not - * typically used directly. One reason to consider using a display is a custom - * {@link View} implementation that needs to issue a large number of drawing commands. - * When the view invalidates, all the drawing commands must be reissued, even if - * large portions of the drawing command stream stay the same frame to frame, which - * can become a performance bottleneck. To solve this issue, a custom View might split - * its content into several display lists. A display list is updated only when its - * content, and only its content, needs to be updated.</p> - * - * <p>A text editor might for instance store each paragraph into its own display list. - * Thus when the user inserts or removes characters, only the display list of the - * affected paragraph needs to be recorded again.</p> - * - * <h3>Hardware acceleration</h3> - * <p>Display lists can only be replayed using a {@link HardwareCanvas}. They are not - * supported in software. Always make sure that the {@link android.graphics.Canvas} - * you are using to render a display list is hardware accelerated using - * {@link android.graphics.Canvas#isHardwareAccelerated()}.</p> - * - * <h3>Creating a display list</h3> - * <pre class="prettyprint"> - * HardwareRenderer renderer = myView.getHardwareRenderer(); - * if (renderer != null) { - * DisplayList displayList = renderer.createDisplayList(); - * HardwareCanvas canvas = displayList.start(width, height); - * try { - * // Draw onto the canvas - * // For instance: canvas.drawBitmap(...); - * } finally { - * displayList.end(); - * } - * } - * </pre> - * - * <h3>Rendering a display list on a View</h3> - * <pre class="prettyprint"> - * protected void onDraw(Canvas canvas) { - * if (canvas.isHardwareAccelerated()) { - * HardwareCanvas hardwareCanvas = (HardwareCanvas) canvas; - * hardwareCanvas.drawDisplayList(mDisplayList); - * } - * } - * </pre> - * - * <h3>Releasing resources</h3> - * <p>This step is not mandatory but recommended if you want to release resources - * held by a display list as soon as possible.</p> - * <pre class="prettyprint"> - * // Mark this display list invalid, it cannot be used for drawing anymore, - * // and release resources held by this display list - * displayList.clear(); - * </pre> - * - * <h3>Properties</h3> - * <p>In addition, a display list offers several properties, such as - * {@link #setScaleX(float)} or {@link #setLeft(int)}, that can be used to affect all - * the drawing commands recorded within. For instance, these properties can be used - * to move around a large number of images without re-issuing all the individual - * <code>drawBitmap()</code> calls.</p> - * - * <pre class="prettyprint"> - * private void createDisplayList() { - * HardwareRenderer renderer = getHardwareRenderer(); - * if (renderer != null) { - * mDisplayList = renderer.createDisplayList(); - * HardwareCanvas canvas = mDisplayList.start(width, height); - * try { - * for (Bitmap b : mBitmaps) { - * canvas.drawBitmap(b, 0.0f, 0.0f, null); - * canvas.translate(0.0f, b.getHeight()); - * } - * } finally { - * displayList.end(); - * } - * } - * } - * - * protected void onDraw(Canvas canvas) { - * if (canvas.isHardwareAccelerated()) { - * HardwareCanvas hardwareCanvas = (HardwareCanvas) canvas; - * hardwareCanvas.drawDisplayList(mDisplayList); - * } - * } - * - * private void moveContentBy(int x) { - * // This will move all the bitmaps recorded inside the display list - * // by x pixels to the right and redraw this view. All the commands - * // recorded in createDisplayList() won't be re-issued, only onDraw() - * // will be invoked and will execute very quickly - * mDisplayList.offsetLeftAndRight(x); - * invalidate(); - * } - * </pre> - * - * <h3>Threading</h3> - * <p>Display lists must be created on and manipulated from the UI thread only.</p> - * - * @hide - */ -public abstract class DisplayList { - private boolean mDirty; - - /** - * Flag used when calling - * {@link HardwareCanvas#drawDisplayList(DisplayList, android.graphics.Rect, int)} - * When this flag is set, draw operations lying outside of the bounds of the - * display list will be culled early. It is recommeneded to always set this - * flag. - * - * @hide - */ - public static final int FLAG_CLIP_CHILDREN = 0x1; - - // NOTE: The STATUS_* values *must* match the enum in DrawGlInfo.h - - /** - * Indicates that the display list is done drawing. - * - * @see HardwareCanvas#drawDisplayList(DisplayList, android.graphics.Rect, int) - * - * @hide - */ - public static final int STATUS_DONE = 0x0; - - /** - * Indicates that the display list needs another drawing pass. - * - * @see HardwareCanvas#drawDisplayList(DisplayList, android.graphics.Rect, int) - * - * @hide - */ - public static final int STATUS_DRAW = 0x1; - - /** - * Indicates that the display list needs to re-execute its GL functors. - * - * @see HardwareCanvas#drawDisplayList(DisplayList, android.graphics.Rect, int) - * @see HardwareCanvas#callDrawGLFunction(int) - * - * @hide - */ - public static final int STATUS_INVOKE = 0x2; - - /** - * Indicates that the display list performed GL drawing operations. - * - * @see HardwareCanvas#drawDisplayList(DisplayList, android.graphics.Rect, int) - * - * @hide - */ - public static final int STATUS_DREW = 0x4; - - /** - * Starts recording the display list. All operations performed on the - * returned canvas are recorded and stored in this display list. - * - * Calling this method will mark the display list invalid until - * {@link #end()} is called. Only valid display lists can be replayed. - * - * @param width The width of the display list's viewport - * @param height The height of the display list's viewport - * - * @return A canvas to record drawing operations. - * - * @see #end() - * @see #isValid() - */ - public abstract HardwareCanvas start(int width, int height); - - /** - * Ends the recording for this display list. A display list cannot be - * replayed if recording is not finished. Calling this method marks - * the display list valid and {@link #isValid()} will return true. - * - * @see #start(int, int) - * @see #isValid() - */ - public abstract void end(); - - /** - * Clears resources held onto by this display list. After calling this method - * {@link #isValid()} will return false. - * - * @see #isValid() - * @see #reset() - */ - public abstract void clear(); - - - /** - * Reset native resources. This is called when cleaning up the state of display lists - * during destruction of hardware resources, to ensure that we do not hold onto - * obsolete resources after related resources are gone. - * - * @see #clear() - * - * @hide - */ - public abstract void reset(); - - /** - * Sets the dirty flag. When a display list is dirty, {@link #clear()} should - * be invoked whenever possible. - * - * @see #isDirty() - * @see #clear() - * - * @hide - */ - public void markDirty() { - mDirty = true; - } - - /** - * Removes the dirty flag. This method can be used to cancel a cleanup - * previously scheduled by setting the dirty flag. - * - * @see #isDirty() - * @see #clear() - * - * @hide - */ - protected void clearDirty() { - mDirty = false; - } - - /** - * Indicates whether the display list is dirty. - * - * @see #markDirty() - * @see #clear() - * - * @hide - */ - public boolean isDirty() { - return mDirty; - } - - /** - * Returns whether the display list is currently usable. If this returns false, - * the display list should be re-recorded prior to replaying it. - * - * @return boolean true if the display list is able to be replayed, false otherwise. - */ - public abstract boolean isValid(); - - /** - * Return the amount of memory used by this display list. - * - * @return The size of this display list in bytes - * - * @hide - */ - public abstract int getSize(); - - /////////////////////////////////////////////////////////////////////////// - // DisplayList Property Setters - /////////////////////////////////////////////////////////////////////////// - - /** - * Set the caching property on the display list, which indicates whether the display list - * holds a layer. Layer display lists should avoid creating an alpha layer, since alpha is - * handled in the drawLayer operation directly (and more efficiently). - * - * @param caching true if the display list represents a hardware layer, false otherwise. - * - * @hide - */ - public abstract void setCaching(boolean caching); - - /** - * Set whether the display list should clip itself to its bounds. This property is controlled by - * the view's parent. - * - * @param clipToBounds true if the display list should clip to its bounds - */ - public abstract void setClipToBounds(boolean clipToBounds); - - /** - * Set the static matrix on the display list. The specified matrix is combined with other - * transforms (such as {@link #setScaleX(float)}, {@link #setRotation(float)}, etc.) - * - * @param matrix A transform matrix to apply to this display list - * - * @see #getMatrix(android.graphics.Matrix) - * @see #getMatrix() - */ - public abstract void setMatrix(Matrix matrix); - - /** - * Returns the static matrix set on this display list. - * - * @return A new {@link Matrix} instance populated with this display list's static - * matrix - * - * @see #getMatrix(android.graphics.Matrix) - * @see #setMatrix(android.graphics.Matrix) - */ - public Matrix getMatrix() { - return getMatrix(new Matrix()); - } - - /** - * Copies this display list's static matrix into the specified matrix. - * - * @param matrix The {@link Matrix} instance in which to copy this display - * list's static matrix. Cannot be null - * - * @return The <code>matrix</code> parameter, for convenience - * - * @see #getMatrix() - * @see #setMatrix(android.graphics.Matrix) - */ - public abstract Matrix getMatrix(Matrix matrix); - - /** - * Set the Animation matrix on the display list. This matrix exists if an Animation is - * currently playing on a View, and is set on the display list during at draw() time. When - * the Animation finishes, the matrix should be cleared by sending <code>null</code> - * for the matrix parameter. - * - * @param matrix The matrix, null indicates that the matrix should be cleared. - * - * @hide - */ - public abstract void setAnimationMatrix(Matrix matrix); - - /** - * Sets the translucency level for the display list. - * - * @param alpha The translucency of the display list, must be a value between 0.0f and 1.0f - * - * @see View#setAlpha(float) - * @see #getAlpha() - */ - public abstract void setAlpha(float alpha); - - /** - * Returns the translucency level of this display list. - * - * @return A value between 0.0f and 1.0f - * - * @see #setAlpha(float) - */ - public abstract float getAlpha(); - - /** - * Sets whether the display list renders content which overlaps. Non-overlapping rendering - * can use a fast path for alpha that avoids rendering to an offscreen buffer. By default - * display lists consider they do not have overlapping content. - * - * @param hasOverlappingRendering False if the content is guaranteed to be non-overlapping, - * true otherwise. - * - * @see android.view.View#hasOverlappingRendering() - * @see #hasOverlappingRendering() - */ - public abstract void setHasOverlappingRendering(boolean hasOverlappingRendering); - - /** - * Indicates whether the content of this display list overlaps. - * - * @return True if this display list renders content which overlaps, false otherwise. - * - * @see #setHasOverlappingRendering(boolean) - */ - public abstract boolean hasOverlappingRendering(); - - /** - * Sets the translation value for the display list on the X axis - * - * @param translationX The X axis translation value of the display list, in pixels - * - * @see View#setTranslationX(float) - * @see #getTranslationX() - */ - public abstract void setTranslationX(float translationX); - - /** - * Returns the translation value for this display list on the X axis, in pixels. - * - * @see #setTranslationX(float) - */ - public abstract float getTranslationX(); - - /** - * Sets the translation value for the display list on the Y axis - * - * @param translationY The Y axis translation value of the display list, in pixels - * - * @see View#setTranslationY(float) - * @see #getTranslationY() - */ - public abstract void setTranslationY(float translationY); - - /** - * Returns the translation value for this display list on the Y axis, in pixels. - * - * @see #setTranslationY(float) - */ - public abstract float getTranslationY(); - - /** - * Sets the rotation value for the display list around the Z axis - * - * @param rotation The rotation value of the display list, in degrees - * - * @see View#setRotation(float) - * @see #getRotation() - */ - public abstract void setRotation(float rotation); - - /** - * Returns the rotation value for this display list around the Z axis, in degrees. - * - * @see #setRotation(float) - */ - public abstract float getRotation(); - - /** - * Sets the rotation value for the display list around the X axis - * - * @param rotationX The rotation value of the display list, in degrees - * - * @see View#setRotationX(float) - * @see #getRotationX() - */ - public abstract void setRotationX(float rotationX); - - /** - * Returns the rotation value for this display list around the X axis, in degrees. - * - * @see #setRotationX(float) - */ - public abstract float getRotationX(); - - /** - * Sets the rotation value for the display list around the Y axis - * - * @param rotationY The rotation value of the display list, in degrees - * - * @see View#setRotationY(float) - * @see #getRotationY() - */ - public abstract void setRotationY(float rotationY); - - /** - * Returns the rotation value for this display list around the Y axis, in degrees. - * - * @see #setRotationY(float) - */ - public abstract float getRotationY(); - - /** - * Sets the scale value for the display list on the X axis - * - * @param scaleX The scale value of the display list - * - * @see View#setScaleX(float) - * @see #getScaleX() - */ - public abstract void setScaleX(float scaleX); - - /** - * Returns the scale value for this display list on the X axis. - * - * @see #setScaleX(float) - */ - public abstract float getScaleX(); - - /** - * Sets the scale value for the display list on the Y axis - * - * @param scaleY The scale value of the display list - * - * @see View#setScaleY(float) - * @see #getScaleY() - */ - public abstract void setScaleY(float scaleY); - - /** - * Returns the scale value for this display list on the Y axis. - * - * @see #setScaleY(float) - */ - public abstract float getScaleY(); - - /** - * Sets all of the transform-related values of the display list - * - * @param alpha The alpha value of the display list - * @param translationX The translationX value of the display list - * @param translationY The translationY value of the display list - * @param rotation The rotation value of the display list - * @param rotationX The rotationX value of the display list - * @param rotationY The rotationY value of the display list - * @param scaleX The scaleX value of the display list - * @param scaleY The scaleY value of the display list - * - * @hide - */ - public abstract void setTransformationInfo(float alpha, float translationX, float translationY, - float rotation, float rotationX, float rotationY, float scaleX, float scaleY); - - /** - * Sets the pivot value for the display list on the X axis - * - * @param pivotX The pivot value of the display list on the X axis, in pixels - * - * @see View#setPivotX(float) - * @see #getPivotX() - */ - public abstract void setPivotX(float pivotX); - - /** - * Returns the pivot value for this display list on the X axis, in pixels. - * - * @see #setPivotX(float) - */ - public abstract float getPivotX(); - - /** - * Sets the pivot value for the display list on the Y axis - * - * @param pivotY The pivot value of the display list on the Y axis, in pixels - * - * @see View#setPivotY(float) - * @see #getPivotY() - */ - public abstract void setPivotY(float pivotY); - - /** - * Returns the pivot value for this display list on the Y axis, in pixels. - * - * @see #setPivotY(float) - */ - public abstract float getPivotY(); - - /** - * Sets the camera distance for the display list. Refer to - * {@link View#setCameraDistance(float)} for more information on how to - * use this property. - * - * @param distance The distance in Z of the camera of the display list - * - * @see View#setCameraDistance(float) - * @see #getCameraDistance() - */ - public abstract void setCameraDistance(float distance); - - /** - * Returns the distance in Z of the camera of the display list. - * - * @see #setCameraDistance(float) - */ - public abstract float getCameraDistance(); - - /** - * Sets the left position for the display list. - * - * @param left The left position, in pixels, of the display list - * - * @see View#setLeft(int) - * @see #getLeft() - */ - public abstract void setLeft(int left); - - /** - * Returns the left position for the display list in pixels. - * - * @see #setLeft(int) - */ - public abstract float getLeft(); - - /** - * Sets the top position for the display list. - * - * @param top The top position, in pixels, of the display list - * - * @see View#setTop(int) - * @see #getTop() - */ - public abstract void setTop(int top); - - /** - * Returns the top position for the display list in pixels. - * - * @see #setTop(int) - */ - public abstract float getTop(); - - /** - * Sets the right position for the display list. - * - * @param right The right position, in pixels, of the display list - * - * @see View#setRight(int) - * @see #getRight() - */ - public abstract void setRight(int right); - - /** - * Returns the right position for the display list in pixels. - * - * @see #setRight(int) - */ - public abstract float getRight(); - - /** - * Sets the bottom position for the display list. - * - * @param bottom The bottom position, in pixels, of the display list - * - * @see View#setBottom(int) - * @see #getBottom() - */ - public abstract void setBottom(int bottom); - - /** - * Returns the bottom position for the display list in pixels. - * - * @see #setBottom(int) - */ - public abstract float getBottom(); - - /** - * Sets the left and top positions for the display list - * - * @param left The left position of the display list, in pixels - * @param top The top position of the display list, in pixels - * @param right The right position of the display list, in pixels - * @param bottom The bottom position of the display list, in pixels - * - * @see View#setLeft(int) - * @see View#setTop(int) - * @see View#setRight(int) - * @see View#setBottom(int) - */ - public abstract void setLeftTopRightBottom(int left, int top, int right, int bottom); - - /** - * Offsets the left and right positions for the display list - * - * @param offset The amount that the left and right positions of the display - * list are offset, in pixels - * - * @see View#offsetLeftAndRight(int) - */ - public abstract void offsetLeftAndRight(float offset); - - /** - * Offsets the top and bottom values for the display list - * - * @param offset The amount that the top and bottom positions of the display - * list are offset, in pixels - * - * @see View#offsetTopAndBottom(int) - */ - public abstract void offsetTopAndBottom(float offset); -} diff --git a/core/java/android/view/FrameStats.java b/core/java/android/view/FrameStats.java new file mode 100644 index 0000000..541b336 --- /dev/null +++ b/core/java/android/view/FrameStats.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2014 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.view; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * This is the base class for frame statistics. + */ +public abstract class FrameStats { + /** + * Undefined time. + */ + public static final long UNDEFINED_TIME_NANO = -1; + + protected long mRefreshPeriodNano; + protected long[] mFramesPresentedTimeNano; + + /** + * Gets the refresh period of the display hosting the window(s) for + * which these statistics apply. + * + * @return The refresh period in nanoseconds. + */ + public final long getRefreshPeriodNano() { + return mRefreshPeriodNano; + } + + /** + * Gets the number of frames for which there is data. + * + * @return The number of frames. + */ + public final int getFrameCount() { + return mFramesPresentedTimeNano != null + ? mFramesPresentedTimeNano.length : 0; + } + + /** + * Gets the start time of the interval for which these statistics + * apply. The start interval is the time when the first frame was + * presented. + * + * @return The start time in nanoseconds or {@link #UNDEFINED_TIME_NANO} + * if there is no frame data. + */ + public final long getStartTimeNano() { + if (getFrameCount() <= 0) { + return UNDEFINED_TIME_NANO; + } + return mFramesPresentedTimeNano[0]; + } + + /** + * Gets the end time of the interval for which these statistics + * apply. The end interval is the time when the last frame was + * presented. + * + * @return The end time in nanoseconds or {@link #UNDEFINED_TIME_NANO} + * if there is no frame data. + */ + public final long getEndTimeNano() { + if (getFrameCount() <= 0) { + return UNDEFINED_TIME_NANO; + } + return mFramesPresentedTimeNano[mFramesPresentedTimeNano.length - 1]; + } + + /** + * Get the time a frame at a given index was presented. + * + * @param index The frame index. + * @return The presented time in nanoseconds or {@link #UNDEFINED_TIME_NANO} + * if the frame is not presented yet. + */ + public final long getFramePresentedTimeNano(int index) { + if (mFramesPresentedTimeNano == null) { + throw new IndexOutOfBoundsException(); + } + return mFramesPresentedTimeNano[index]; + } +} diff --git a/core/java/android/view/GLES20Canvas.java b/core/java/android/view/GLES20Canvas.java index d533060..c274fc4 100644 --- a/core/java/android/view/GLES20Canvas.java +++ b/core/java/android/view/GLES20Canvas.java @@ -18,7 +18,6 @@ package android.view; import android.graphics.Bitmap; import android.graphics.Canvas; -import android.graphics.ColorFilter; import android.graphics.DrawFilter; import android.graphics.Matrix; import android.graphics.NinePatch; @@ -31,7 +30,6 @@ import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Region; import android.graphics.Shader; -import android.graphics.SurfaceTexture; import android.graphics.TemporaryBuffer; import android.text.GraphicsOperations; import android.text.SpannableString; @@ -46,10 +44,9 @@ class GLES20Canvas extends HardwareCanvas { private static final int MODIFIER_NONE = 0; private static final int MODIFIER_SHADOW = 1; private static final int MODIFIER_SHADER = 2; - private static final int MODIFIER_COLOR_FILTER = 4; private final boolean mOpaque; - private long mRenderer; + protected long mRenderer; // The native renderer will be destroyed when this object dies. // DO NOT overwrite this reference once it is set. @@ -88,15 +85,6 @@ class GLES20Canvas extends HardwareCanvas { GLES20Canvas(boolean translucent) { this(false, translucent); } - - /** - * Creates a canvas to render into an FBO. - */ - GLES20Canvas(long layer, boolean translucent) { - mOpaque = !translucent; - mRenderer = nCreateLayerRenderer(layer); - setupFinalizer(); - } protected GLES20Canvas(boolean record, boolean translucent) { mOpaque = !translucent; @@ -118,12 +106,7 @@ class GLES20Canvas extends HardwareCanvas { } } - protected void resetDisplayListRenderer() { - nResetDisplayListRenderer(mRenderer); - } - private static native long nCreateRenderer(); - private static native long nCreateLayerRenderer(long layer); private static native long nCreateDisplayListRenderer(); private static native void nResetDisplayListRenderer(long renderer); private static native void nDestroyRenderer(long renderer); @@ -145,13 +128,11 @@ class GLES20Canvas extends HardwareCanvas { } } - @Override - public void setName(String name) { - super.setName(name); - nSetName(mRenderer, name); + public static void setProperty(String name, String value) { + nSetProperty(name, value); } - private static native void nSetName(long renderer, String name); + private static native void nSetProperty(String name, String value); /////////////////////////////////////////////////////////////////////////// // Hardware layers @@ -159,12 +140,12 @@ class GLES20Canvas extends HardwareCanvas { @Override void pushLayerUpdate(HardwareLayer layer) { - nPushLayerUpdate(mRenderer, ((GLES20RenderLayer) layer).mLayer); + nPushLayerUpdate(mRenderer, layer.getLayer()); } @Override void cancelLayerUpdate(HardwareLayer layer) { - nCancelLayerUpdate(mRenderer, ((GLES20RenderLayer) layer).mLayer); + nCancelLayerUpdate(mRenderer, layer.getLayer()); } @Override @@ -177,22 +158,7 @@ class GLES20Canvas extends HardwareCanvas { nClearLayerUpdates(mRenderer); } - static native long nCreateTextureLayer(boolean opaque, int[] layerInfo); - static native long nCreateLayer(int width, int height, boolean isOpaque, int[] layerInfo); - static native boolean nResizeLayer(long layerId, int width, int height, int[] layerInfo); - static native void nSetOpaqueLayer(long layerId, boolean isOpaque); - static native void nSetLayerPaint(long layerId, long nativePaint); - static native void nSetLayerColorFilter(long layerId, long nativeColorFilter); - static native void nUpdateTextureLayer(long layerId, int width, int height, boolean opaque, - SurfaceTexture surface); - static native void nClearLayerTexture(long layerId); - static native void nSetTextureLayerTransform(long layerId, long matrix); - static native void nDestroyLayer(long layerId); - static native void nDestroyLayerDeferred(long layerId); - static native void nUpdateRenderLayer(long layerId, long renderer, long displayList, - int left, int top, int right, int bottom); static native boolean nCopyLayer(long layerId, long bitmap); - private static native void nClearLayerUpdates(long renderer); private static native void nFlushLayerUpdates(long renderer); private static native void nPushLayerUpdate(long renderer, long layer); @@ -286,18 +252,6 @@ class GLES20Canvas extends HardwareCanvas { private static native int nGetStencilSize(); - void setCountOverdrawEnabled(boolean enabled) { - nSetCountOverdrawEnabled(mRenderer, enabled); - } - - static native void nSetCountOverdrawEnabled(long renderer, boolean enabled); - - float getOverdraw() { - return nGetOverdraw(mRenderer); - } - - static native float nGetOverdraw(long renderer); - /////////////////////////////////////////////////////////////////////////// // Functor /////////////////////////////////////////////////////////////////////////// @@ -402,22 +356,11 @@ class GLES20Canvas extends HardwareCanvas { // Display list /////////////////////////////////////////////////////////////////////////// - long getDisplayList(long displayList) { - return nGetDisplayList(mRenderer, displayList); - } - - private static native long nGetDisplayList(long renderer, long displayList); + protected static native long nFinishRecording(long renderer); @Override - void outputDisplayList(DisplayList displayList) { - nOutputDisplayList(mRenderer, ((GLES20DisplayList) displayList).getNativeDisplayList()); - } - - private static native void nOutputDisplayList(long renderer, long displayList); - - @Override - public int drawDisplayList(DisplayList displayList, Rect dirty, int flags) { - return nDrawDisplayList(mRenderer, ((GLES20DisplayList) displayList).getNativeDisplayList(), + public int drawDisplayList(RenderNode displayList, Rect dirty, int flags) { + return nDrawDisplayList(mRenderer, displayList.getNativeDisplayList(), dirty, flags); } @@ -430,9 +373,7 @@ class GLES20Canvas extends HardwareCanvas { void drawHardwareLayer(HardwareLayer layer, float x, float y, Paint paint) { layer.setLayerPaint(paint); - - final GLES20Layer glLayer = (GLES20Layer) layer; - nDrawLayer(mRenderer, glLayer.getLayer(), x, y); + nDrawLayer(mRenderer, layer.getLayer(), x, y); } private static native void nDrawLayer(long renderer, long layer, float x, float y); @@ -648,15 +589,8 @@ class GLES20Canvas extends HardwareCanvas { return saveLayer(bounds.left, bounds.top, bounds.right, bounds.bottom, paint, saveFlags); } - int count; - int modifier = paint != null ? setupColorFilter(paint) : MODIFIER_NONE; - try { - final long nativePaint = paint == null ? 0 : paint.mNativePaint; - count = nSaveLayer(mRenderer, nativePaint, saveFlags); - } finally { - if (modifier != MODIFIER_NONE) nResetModifiers(mRenderer, modifier); - } - return count; + final long nativePaint = paint == null ? 0 : paint.mNativePaint; + return nSaveLayer(mRenderer, nativePaint, saveFlags); } private static native int nSaveLayer(long renderer, long paint, int saveFlags); @@ -665,15 +599,8 @@ class GLES20Canvas extends HardwareCanvas { public int saveLayer(float left, float top, float right, float bottom, Paint paint, int saveFlags) { if (left < right && top < bottom) { - int count; - int modifier = paint != null ? setupColorFilter(paint) : MODIFIER_NONE; - try { - final long nativePaint = paint == null ? 0 : paint.mNativePaint; - count = nSaveLayer(mRenderer, left, top, right, bottom, nativePaint, saveFlags); - } finally { - if (modifier != MODIFIER_NONE) nResetModifiers(mRenderer, modifier); - } - return count; + final long nativePaint = paint == null ? 0 : paint.mNativePaint; + return nSaveLayer(mRenderer, left, top, right, bottom, nativePaint, saveFlags); } return save(saveFlags); } @@ -755,7 +682,7 @@ class GLES20Canvas extends HardwareCanvas { @Override public void drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint) { - int modifiers = setupModifiers(paint, MODIFIER_COLOR_FILTER | MODIFIER_SHADER); + int modifiers = setupModifiers(paint, MODIFIER_SHADER); try { nDrawArc(mRenderer, oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle, useCenter, paint.mNativePaint); @@ -778,14 +705,9 @@ class GLES20Canvas extends HardwareCanvas { Bitmap bitmap = patch.getBitmap(); throwIfCannotDraw(bitmap); // Shaders are ignored when drawing patches - int modifier = paint != null ? setupColorFilter(paint) : MODIFIER_NONE; - try { - final long nativePaint = paint == null ? 0 : paint.mNativePaint; - nDrawPatch(mRenderer, bitmap.mNativeBitmap, bitmap.mBuffer, patch.mNativeChunk, - dst.left, dst.top, dst.right, dst.bottom, nativePaint); - } finally { - if (modifier != MODIFIER_NONE) nResetModifiers(mRenderer, modifier); - } + final long nativePaint = paint == null ? 0 : paint.mNativePaint; + nDrawPatch(mRenderer, bitmap.mNativeBitmap, bitmap.mBuffer, patch.mNativeChunk, + dst.left, dst.top, dst.right, dst.bottom, nativePaint); } @Override @@ -793,14 +715,9 @@ class GLES20Canvas extends HardwareCanvas { Bitmap bitmap = patch.getBitmap(); throwIfCannotDraw(bitmap); // Shaders are ignored when drawing patches - int modifier = paint != null ? setupColorFilter(paint) : MODIFIER_NONE; - try { - final long nativePaint = paint == null ? 0 : paint.mNativePaint; - nDrawPatch(mRenderer, bitmap.mNativeBitmap, bitmap.mBuffer, patch.mNativeChunk, - dst.left, dst.top, dst.right, dst.bottom, nativePaint); - } finally { - if (modifier != MODIFIER_NONE) nResetModifiers(mRenderer, modifier); - } + final long nativePaint = paint == null ? 0 : paint.mNativePaint; + nDrawPatch(mRenderer, bitmap.mNativeBitmap, bitmap.mBuffer, patch.mNativeChunk, + dst.left, dst.top, dst.right, dst.bottom, nativePaint); } private static native void nDrawPatch(long renderer, long bitmap, byte[] buffer, long chunk, @@ -921,14 +838,9 @@ class GLES20Canvas extends HardwareCanvas { } // Shaders are ignored when drawing bitmaps - int modifier = paint != null ? setupColorFilter(paint) : MODIFIER_NONE; - try { - final long nativePaint = paint == null ? 0 : paint.mNativePaint; - nDrawBitmap(mRenderer, colors, offset, stride, x, y, - width, height, hasAlpha, nativePaint); - } finally { - if (modifier != MODIFIER_NONE) nResetModifiers(mRenderer, modifier); - } + final long nativePaint = paint == null ? 0 : paint.mNativePaint; + nDrawBitmap(mRenderer, colors, offset, stride, x, y, + width, height, hasAlpha, nativePaint); } private static native void nDrawBitmap(long renderer, int[] colors, int offset, int stride, @@ -976,7 +888,7 @@ class GLES20Canvas extends HardwareCanvas { @Override public void drawCircle(float cx, float cy, float radius, Paint paint) { - int modifiers = setupModifiers(paint, MODIFIER_COLOR_FILTER | MODIFIER_SHADER); + int modifiers = setupModifiers(paint, MODIFIER_SHADER); try { nDrawCircle(mRenderer, cx, cy, radius, paint.mNativePaint); } finally { @@ -1016,7 +928,7 @@ class GLES20Canvas extends HardwareCanvas { if ((offset | count) < 0 || offset + count > pts.length) { throw new IllegalArgumentException("The lines array must contain 4 elements per line."); } - int modifiers = setupModifiers(paint, MODIFIER_COLOR_FILTER | MODIFIER_SHADER); + int modifiers = setupModifiers(paint, MODIFIER_SHADER); try { nDrawLines(mRenderer, pts, offset, count, paint.mNativePaint); } finally { @@ -1034,7 +946,7 @@ class GLES20Canvas extends HardwareCanvas { @Override public void drawOval(RectF oval, Paint paint) { - int modifiers = setupModifiers(paint, MODIFIER_COLOR_FILTER | MODIFIER_SHADER); + int modifiers = setupModifiers(paint, MODIFIER_SHADER); try { nDrawOval(mRenderer, oval.left, oval.top, oval.right, oval.bottom, paint.mNativePaint); } finally { @@ -1054,7 +966,7 @@ class GLES20Canvas extends HardwareCanvas { @Override public void drawPath(Path path, Paint paint) { - int modifiers = setupModifiers(paint, MODIFIER_COLOR_FILTER | MODIFIER_SHADER); + int modifiers = setupModifiers(paint, MODIFIER_SHADER); try { if (path.isSimplePath) { if (path.rects != null) { @@ -1072,7 +984,7 @@ class GLES20Canvas extends HardwareCanvas { private static native void nDrawRects(long renderer, long region, long paint); void drawRects(float[] rects, int count, Paint paint) { - int modifiers = setupModifiers(paint, MODIFIER_COLOR_FILTER | MODIFIER_SHADER); + int modifiers = setupModifiers(paint, MODIFIER_SHADER); try { nDrawRects(mRenderer, rects, count, paint.mNativePaint); } finally { @@ -1139,7 +1051,7 @@ class GLES20Canvas extends HardwareCanvas { public void drawPoints(float[] pts, int offset, int count, Paint paint) { if (count < 2) return; - int modifiers = setupModifiers(paint, MODIFIER_COLOR_FILTER | MODIFIER_SHADER); + int modifiers = setupModifiers(paint, MODIFIER_SHADER); try { nDrawPoints(mRenderer, pts, offset, count, paint.mNativePaint); } finally { @@ -1189,7 +1101,7 @@ class GLES20Canvas extends HardwareCanvas { @Override public void drawRect(float left, float top, float right, float bottom, Paint paint) { if (left == right || top == bottom) return; - int modifiers = setupModifiers(paint, MODIFIER_COLOR_FILTER | MODIFIER_SHADER); + int modifiers = setupModifiers(paint, MODIFIER_SHADER); try { nDrawRect(mRenderer, left, top, right, bottom, paint.mNativePaint); } finally { @@ -1217,7 +1129,7 @@ class GLES20Canvas extends HardwareCanvas { @Override public void drawRoundRect(RectF rect, float rx, float ry, Paint paint) { - int modifiers = setupModifiers(paint, MODIFIER_COLOR_FILTER | MODIFIER_SHADER); + int modifiers = setupModifiers(paint, MODIFIER_SHADER); try { nDrawRoundRect(mRenderer, rect.left, rect.top, rect.right, rect.bottom, rx, ry, paint.mNativePaint); @@ -1397,12 +1309,6 @@ class GLES20Canvas extends HardwareCanvas { private int setupModifiers(Bitmap b, Paint paint) { if (b.getConfig() != Bitmap.Config.ALPHA_8) { - final ColorFilter filter = paint.getColorFilter(); - if (filter != null) { - nSetupColorFilter(mRenderer, filter.nativeColorFilter); - return MODIFIER_COLOR_FILTER; - } - return MODIFIER_NONE; } else { return setupModifiers(paint); @@ -1424,12 +1330,6 @@ class GLES20Canvas extends HardwareCanvas { modifiers |= MODIFIER_SHADER; } - final ColorFilter filter = paint.getColorFilter(); - if (filter != null) { - nSetupColorFilter(mRenderer, filter.nativeColorFilter); - modifiers |= MODIFIER_COLOR_FILTER; - } - return modifiers; } @@ -1448,26 +1348,10 @@ class GLES20Canvas extends HardwareCanvas { modifiers |= MODIFIER_SHADER; } - final ColorFilter filter = paint.getColorFilter(); - if (filter != null && (flags & MODIFIER_COLOR_FILTER) != 0) { - nSetupColorFilter(mRenderer, filter.nativeColorFilter); - modifiers |= MODIFIER_COLOR_FILTER; - } - return modifiers; } - private int setupColorFilter(Paint paint) { - final ColorFilter filter = paint.getColorFilter(); - if (filter != null) { - nSetupColorFilter(mRenderer, filter.nativeColorFilter); - return MODIFIER_COLOR_FILTER; - } - return MODIFIER_NONE; - } - private static native void nSetupShader(long renderer, long shader); - private static native void nSetupColorFilter(long renderer, long colorFilter); private static native void nSetupShadow(long renderer, float radius, float dx, float dy, int color); diff --git a/core/java/android/view/GLES20DisplayList.java b/core/java/android/view/GLES20DisplayList.java deleted file mode 100644 index 7f8b3bd..0000000 --- a/core/java/android/view/GLES20DisplayList.java +++ /dev/null @@ -1,511 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.view; - -import android.graphics.Matrix; - -import java.util.ArrayList; - -/** - * An implementation of display list for OpenGL ES 2.0. - */ -class GLES20DisplayList extends DisplayList { - private ArrayList<DisplayList> mChildDisplayLists; - - private GLES20RecordingCanvas mCanvas; - private boolean mValid; - - // Used for debugging - private final String mName; - - // The native display list will be destroyed when this object dies. - // DO NOT overwrite this reference once it is set. - private DisplayListFinalizer mFinalizer; - - GLES20DisplayList(String name) { - mName = name; - } - - boolean hasNativeDisplayList() { - return mValid && mFinalizer != null; - } - - long getNativeDisplayList() { - if (!mValid || mFinalizer == null) { - throw new IllegalStateException("The display list is not valid."); - } - return mFinalizer.mNativeDisplayList; - } - - @Override - public HardwareCanvas start(int width, int height) { - if (mCanvas != null) { - throw new IllegalStateException("Recording has already started"); - } - - mValid = false; - mCanvas = GLES20RecordingCanvas.obtain(this); - mCanvas.start(); - - mCanvas.setViewport(width, height); - // The dirty rect should always be null for a display list - mCanvas.onPreDraw(null); - - return mCanvas; - } - @Override - public void clear() { - clearDirty(); - - if (mCanvas != null) { - mCanvas.recycle(); - mCanvas = null; - } - mValid = false; - - clearReferences(); - } - - void clearReferences() { - if (mChildDisplayLists != null) mChildDisplayLists.clear(); - } - - ArrayList<DisplayList> getChildDisplayLists() { - if (mChildDisplayLists == null) mChildDisplayLists = new ArrayList<DisplayList>(); - return mChildDisplayLists; - } - - @Override - public void reset() { - if (hasNativeDisplayList()) { - nReset(mFinalizer.mNativeDisplayList); - } - clear(); - } - - @Override - public boolean isValid() { - return mValid; - } - - @Override - public void end() { - if (mCanvas != null) { - mCanvas.onPostDraw(); - if (mFinalizer != null) { - mCanvas.end(mFinalizer.mNativeDisplayList); - } else { - mFinalizer = new DisplayListFinalizer(mCanvas.end(0)); - nSetDisplayListName(mFinalizer.mNativeDisplayList, mName); - } - mCanvas.recycle(); - mCanvas = null; - mValid = true; - } - } - - @Override - public int getSize() { - if (mFinalizer == null) return 0; - return nGetDisplayListSize(mFinalizer.mNativeDisplayList); - } - - private static native void nDestroyDisplayList(long displayList); - private static native int nGetDisplayListSize(long displayList); - private static native void nSetDisplayListName(long displayList, String name); - - /////////////////////////////////////////////////////////////////////////// - // Native View Properties - /////////////////////////////////////////////////////////////////////////// - - @Override - public void setCaching(boolean caching) { - if (hasNativeDisplayList()) { - nSetCaching(mFinalizer.mNativeDisplayList, caching); - } - } - - @Override - public void setClipToBounds(boolean clipToBounds) { - if (hasNativeDisplayList()) { - nSetClipToBounds(mFinalizer.mNativeDisplayList, clipToBounds); - } - } - - @Override - public void setMatrix(Matrix matrix) { - if (hasNativeDisplayList()) { - nSetStaticMatrix(mFinalizer.mNativeDisplayList, matrix.native_instance); - } - } - - @Override - public Matrix getMatrix(Matrix matrix) { - if (hasNativeDisplayList()) { - nGetMatrix(mFinalizer.mNativeDisplayList, matrix.native_instance); - } - return matrix; - } - - @Override - public void setAnimationMatrix(Matrix matrix) { - if (hasNativeDisplayList()) { - nSetAnimationMatrix(mFinalizer.mNativeDisplayList, - (matrix != null) ? matrix.native_instance : 0); - } - } - - @Override - public void setAlpha(float alpha) { - if (hasNativeDisplayList()) { - nSetAlpha(mFinalizer.mNativeDisplayList, alpha); - } - } - - @Override - public float getAlpha() { - if (hasNativeDisplayList()) { - return nGetAlpha(mFinalizer.mNativeDisplayList); - } - return 1.0f; - } - - @Override - public void setHasOverlappingRendering(boolean hasOverlappingRendering) { - if (hasNativeDisplayList()) { - nSetHasOverlappingRendering(mFinalizer.mNativeDisplayList, hasOverlappingRendering); - } - } - - @Override - public boolean hasOverlappingRendering() { - //noinspection SimplifiableIfStatement - if (hasNativeDisplayList()) { - return nHasOverlappingRendering(mFinalizer.mNativeDisplayList); - } - return true; - } - - @Override - public void setTranslationX(float translationX) { - if (hasNativeDisplayList()) { - nSetTranslationX(mFinalizer.mNativeDisplayList, translationX); - } - } - - @Override - public float getTranslationX() { - if (hasNativeDisplayList()) { - return nGetTranslationX(mFinalizer.mNativeDisplayList); - } - return 0.0f; - } - - @Override - public void setTranslationY(float translationY) { - if (hasNativeDisplayList()) { - nSetTranslationY(mFinalizer.mNativeDisplayList, translationY); - } - } - - @Override - public float getTranslationY() { - if (hasNativeDisplayList()) { - return nGetTranslationY(mFinalizer.mNativeDisplayList); - } - return 0.0f; - } - - @Override - public void setRotation(float rotation) { - if (hasNativeDisplayList()) { - nSetRotation(mFinalizer.mNativeDisplayList, rotation); - } - } - - @Override - public float getRotation() { - if (hasNativeDisplayList()) { - return nGetRotation(mFinalizer.mNativeDisplayList); - } - return 0.0f; - } - - @Override - public void setRotationX(float rotationX) { - if (hasNativeDisplayList()) { - nSetRotationX(mFinalizer.mNativeDisplayList, rotationX); - } - } - - @Override - public float getRotationX() { - if (hasNativeDisplayList()) { - return nGetRotationX(mFinalizer.mNativeDisplayList); - } - return 0.0f; - } - - @Override - public void setRotationY(float rotationY) { - if (hasNativeDisplayList()) { - nSetRotationY(mFinalizer.mNativeDisplayList, rotationY); - } - } - - @Override - public float getRotationY() { - if (hasNativeDisplayList()) { - return nGetRotationY(mFinalizer.mNativeDisplayList); - } - return 0.0f; - } - - @Override - public void setScaleX(float scaleX) { - if (hasNativeDisplayList()) { - nSetScaleX(mFinalizer.mNativeDisplayList, scaleX); - } - } - - @Override - public float getScaleX() { - if (hasNativeDisplayList()) { - return nGetScaleX(mFinalizer.mNativeDisplayList); - } - return 1.0f; - } - - @Override - public void setScaleY(float scaleY) { - if (hasNativeDisplayList()) { - nSetScaleY(mFinalizer.mNativeDisplayList, scaleY); - } - } - - @Override - public float getScaleY() { - if (hasNativeDisplayList()) { - return nGetScaleY(mFinalizer.mNativeDisplayList); - } - return 1.0f; - } - - @Override - public void setTransformationInfo(float alpha, float translationX, float translationY, - float rotation, float rotationX, float rotationY, float scaleX, float scaleY) { - if (hasNativeDisplayList()) { - nSetTransformationInfo(mFinalizer.mNativeDisplayList, alpha, translationX, translationY, - rotation, rotationX, rotationY, scaleX, scaleY); - } - } - - @Override - public void setPivotX(float pivotX) { - if (hasNativeDisplayList()) { - nSetPivotX(mFinalizer.mNativeDisplayList, pivotX); - } - } - - @Override - public float getPivotX() { - if (hasNativeDisplayList()) { - return nGetPivotX(mFinalizer.mNativeDisplayList); - } - return 0.0f; - } - - @Override - public void setPivotY(float pivotY) { - if (hasNativeDisplayList()) { - nSetPivotY(mFinalizer.mNativeDisplayList, pivotY); - } - } - - @Override - public float getPivotY() { - if (hasNativeDisplayList()) { - return nGetPivotY(mFinalizer.mNativeDisplayList); - } - return 0.0f; - } - - @Override - public void setCameraDistance(float distance) { - if (hasNativeDisplayList()) { - nSetCameraDistance(mFinalizer.mNativeDisplayList, distance); - } - } - - @Override - public float getCameraDistance() { - if (hasNativeDisplayList()) { - return nGetCameraDistance(mFinalizer.mNativeDisplayList); - } - return 0.0f; - } - - @Override - public void setLeft(int left) { - if (hasNativeDisplayList()) { - nSetLeft(mFinalizer.mNativeDisplayList, left); - } - } - - @Override - public float getLeft() { - if (hasNativeDisplayList()) { - return nGetLeft(mFinalizer.mNativeDisplayList); - } - return 0.0f; - } - - @Override - public void setTop(int top) { - if (hasNativeDisplayList()) { - nSetTop(mFinalizer.mNativeDisplayList, top); - } - } - - @Override - public float getTop() { - if (hasNativeDisplayList()) { - return nGetTop(mFinalizer.mNativeDisplayList); - } - return 0.0f; - } - - @Override - public void setRight(int right) { - if (hasNativeDisplayList()) { - nSetRight(mFinalizer.mNativeDisplayList, right); - } - } - - @Override - public float getRight() { - if (hasNativeDisplayList()) { - return nGetRight(mFinalizer.mNativeDisplayList); - } - return 0.0f; - } - - @Override - public void setBottom(int bottom) { - if (hasNativeDisplayList()) { - nSetBottom(mFinalizer.mNativeDisplayList, bottom); - } - } - - @Override - public float getBottom() { - if (hasNativeDisplayList()) { - return nGetBottom(mFinalizer.mNativeDisplayList); - } - return 0.0f; - } - - @Override - public void setLeftTopRightBottom(int left, int top, int right, int bottom) { - if (hasNativeDisplayList()) { - nSetLeftTopRightBottom(mFinalizer.mNativeDisplayList, left, top, right, bottom); - } - } - - @Override - public void offsetLeftAndRight(float offset) { - if (hasNativeDisplayList()) { - nOffsetLeftAndRight(mFinalizer.mNativeDisplayList, offset); - } - } - - @Override - public void offsetTopAndBottom(float offset) { - if (hasNativeDisplayList()) { - nOffsetTopAndBottom(mFinalizer.mNativeDisplayList, offset); - } - } - - private static native void nReset(long displayList); - private static native void nOffsetTopAndBottom(long displayList, float offset); - private static native void nOffsetLeftAndRight(long displayList, float offset); - private static native void nSetLeftTopRightBottom(long displayList, int left, int top, - int right, int bottom); - private static native void nSetBottom(long displayList, int bottom); - private static native void nSetRight(long displayList, int right); - private static native void nSetTop(long displayList, int top); - private static native void nSetLeft(long displayList, int left); - private static native void nSetCameraDistance(long displayList, float distance); - private static native void nSetPivotY(long displayList, float pivotY); - private static native void nSetPivotX(long displayList, float pivotX); - private static native void nSetCaching(long displayList, boolean caching); - private static native void nSetClipToBounds(long displayList, boolean clipToBounds); - private static native void nSetAlpha(long displayList, float alpha); - private static native void nSetHasOverlappingRendering(long displayList, - boolean hasOverlappingRendering); - private static native void nSetTranslationX(long displayList, float translationX); - private static native void nSetTranslationY(long displayList, float translationY); - private static native void nSetRotation(long displayList, float rotation); - private static native void nSetRotationX(long displayList, float rotationX); - private static native void nSetRotationY(long displayList, float rotationY); - private static native void nSetScaleX(long displayList, float scaleX); - private static native void nSetScaleY(long displayList, float scaleY); - private static native void nSetTransformationInfo(long displayList, float alpha, - float translationX, float translationY, float rotation, float rotationX, - float rotationY, float scaleX, float scaleY); - private static native void nSetStaticMatrix(long displayList, long nativeMatrix); - private static native void nSetAnimationMatrix(long displayList, long animationMatrix); - - private static native boolean nHasOverlappingRendering(long displayList); - private static native void nGetMatrix(long displayList, long matrix); - private static native float nGetAlpha(long displayList); - private static native float nGetLeft(long displayList); - private static native float nGetTop(long displayList); - private static native float nGetRight(long displayList); - private static native float nGetBottom(long displayList); - private static native float nGetCameraDistance(long displayList); - private static native float nGetScaleX(long displayList); - private static native float nGetScaleY(long displayList); - private static native float nGetTranslationX(long displayList); - private static native float nGetTranslationY(long displayList); - private static native float nGetRotation(long displayList); - private static native float nGetRotationX(long displayList); - private static native float nGetRotationY(long displayList); - private static native float nGetPivotX(long displayList); - private static native float nGetPivotY(long displayList); - - /////////////////////////////////////////////////////////////////////////// - // Finalization - /////////////////////////////////////////////////////////////////////////// - - private static class DisplayListFinalizer { - final long mNativeDisplayList; - - public DisplayListFinalizer(long nativeDisplayList) { - mNativeDisplayList = nativeDisplayList; - } - - @Override - protected void finalize() throws Throwable { - try { - nDestroyDisplayList(mNativeDisplayList); - } finally { - super.finalize(); - } - } - } -} diff --git a/core/java/android/view/GLES20Layer.java b/core/java/android/view/GLES20Layer.java deleted file mode 100644 index 37154eb..0000000 --- a/core/java/android/view/GLES20Layer.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (C) 2011 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.view; - -import android.graphics.Bitmap; -import android.graphics.Paint; - -/** - * An OpenGL ES 2.0 implementation of {@link HardwareLayer}. - */ -abstract class GLES20Layer extends HardwareLayer { - long mLayer; - Finalizer mFinalizer; - - GLES20Layer() { - } - - GLES20Layer(int width, int height, boolean opaque) { - super(width, height, opaque); - } - - /** - * Returns the native layer object used to render this layer. - * - * @return A pointer to the native layer object, or 0 if the object is NULL - */ - public long getLayer() { - return mLayer; - } - - @Override - void setLayerPaint(Paint paint) { - if (paint != null) { - GLES20Canvas.nSetLayerPaint(mLayer, paint.mNativePaint); - GLES20Canvas.nSetLayerColorFilter(mLayer, paint.getColorFilter() != null ? - paint.getColorFilter().nativeColorFilter : 0); - } - } - - @Override - public boolean copyInto(Bitmap bitmap) { - return GLES20Canvas.nCopyLayer(mLayer, bitmap.mNativeBitmap); - } - - @Override - public void destroy() { - if (mDisplayList != null) { - mDisplayList.reset(); - } - if (mFinalizer != null) { - mFinalizer.destroy(); - mFinalizer = null; - } - mLayer = 0; - } - - @Override - void clearStorage() { - if (mLayer != 0) GLES20Canvas.nClearLayerTexture(mLayer); - } - - static class Finalizer { - private long mLayerId; - - public Finalizer(long layerId) { - mLayerId = layerId; - } - - @Override - protected void finalize() throws Throwable { - try { - if (mLayerId != 0) { - GLES20Canvas.nDestroyLayerDeferred(mLayerId); - } - } finally { - super.finalize(); - } - } - - void destroy() { - GLES20Canvas.nDestroyLayer(mLayerId); - mLayerId = 0; - } - } -} diff --git a/core/java/android/view/GLES20RecordingCanvas.java b/core/java/android/view/GLES20RecordingCanvas.java index e3e1c76..2b29e5c 100644 --- a/core/java/android/view/GLES20RecordingCanvas.java +++ b/core/java/android/view/GLES20RecordingCanvas.java @@ -16,7 +16,6 @@ package android.view; -import android.graphics.Rect; import android.util.Pools.SynchronizedPool; /** @@ -33,39 +32,23 @@ class GLES20RecordingCanvas extends GLES20Canvas { private static final SynchronizedPool<GLES20RecordingCanvas> sPool = new SynchronizedPool<GLES20RecordingCanvas>(POOL_LIMIT); - private GLES20DisplayList mDisplayList; - private GLES20RecordingCanvas() { super(true, true); } - static GLES20RecordingCanvas obtain(GLES20DisplayList displayList) { + static GLES20RecordingCanvas obtain() { GLES20RecordingCanvas canvas = sPool.acquire(); if (canvas == null) { canvas = new GLES20RecordingCanvas(); } - canvas.mDisplayList = displayList; return canvas; } void recycle() { - mDisplayList = null; - resetDisplayListRenderer(); sPool.release(this); } - void start() { - mDisplayList.clearReferences(); - } - - long end(long nativeDisplayList) { - return getDisplayList(nativeDisplayList); - } - - @Override - public int drawDisplayList(DisplayList displayList, Rect dirty, int flags) { - int status = super.drawDisplayList(displayList, dirty, flags); - mDisplayList.getChildDisplayLists().add(displayList); - return status; + long finishRecording() { + return nFinishRecording(mRenderer); } } diff --git a/core/java/android/view/GLES20RenderLayer.java b/core/java/android/view/GLES20RenderLayer.java deleted file mode 100644 index 68ba77c..0000000 --- a/core/java/android/view/GLES20RenderLayer.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright (C) 2011 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.view; - -import android.graphics.Canvas; -import android.graphics.Matrix; -import android.graphics.Rect; - -/** - * An OpenGL ES 2.0 implementation of {@link HardwareLayer}. This - * implementation can be used a rendering target. It generates a - * {@link Canvas} that can be used to render into an FBO using OpenGL. - */ -class GLES20RenderLayer extends GLES20Layer { - private int mLayerWidth; - private int mLayerHeight; - - private final GLES20Canvas mCanvas; - - GLES20RenderLayer(int width, int height, boolean isOpaque) { - super(width, height, isOpaque); - - int[] layerInfo = new int[2]; - mLayer = GLES20Canvas.nCreateLayer(width, height, isOpaque, layerInfo); - if (mLayer != 0) { - mLayerWidth = layerInfo[0]; - mLayerHeight = layerInfo[1]; - - mCanvas = new GLES20Canvas(mLayer, !isOpaque); - mFinalizer = new Finalizer(mLayer); - } else { - mCanvas = null; - mFinalizer = null; - } - } - - @Override - boolean isValid() { - return mLayer != 0 && mLayerWidth > 0 && mLayerHeight > 0; - } - - @Override - boolean resize(int width, int height) { - if (!isValid() || width <= 0 || height <= 0) return false; - - mWidth = width; - mHeight = height; - - if (width != mLayerWidth || height != mLayerHeight) { - int[] layerInfo = new int[2]; - - if (GLES20Canvas.nResizeLayer(mLayer, width, height, layerInfo)) { - mLayerWidth = layerInfo[0]; - mLayerHeight = layerInfo[1]; - } else { - // Failure: not enough GPU resources for requested size - mLayer = 0; - mLayerWidth = 0; - mLayerHeight = 0; - } - } - return isValid(); - } - - @Override - void setOpaque(boolean isOpaque) { - mOpaque = isOpaque; - GLES20Canvas.nSetOpaqueLayer(mLayer, isOpaque); - } - - @Override - HardwareCanvas getCanvas() { - return mCanvas; - } - - @Override - void end(Canvas currentCanvas) { - HardwareCanvas canvas = getCanvas(); - if (canvas != null) { - canvas.onPostDraw(); - } - if (currentCanvas instanceof GLES20Canvas) { - ((GLES20Canvas) currentCanvas).resume(); - } - } - - @Override - HardwareCanvas start(Canvas currentCanvas) { - return start(currentCanvas, null); - } - - @Override - HardwareCanvas start(Canvas currentCanvas, Rect dirty) { - if (currentCanvas instanceof GLES20Canvas) { - ((GLES20Canvas) currentCanvas).interrupt(); - } - HardwareCanvas canvas = getCanvas(); - canvas.setViewport(mWidth, mHeight); - canvas.onPreDraw(dirty); - return canvas; - } - - /** - * Ignored - */ - @Override - void setTransform(Matrix matrix) { - } - - @Override - void redrawLater(DisplayList displayList, Rect dirtyRect) { - GLES20Canvas.nUpdateRenderLayer(mLayer, mCanvas.getRenderer(), - ((GLES20DisplayList) displayList).getNativeDisplayList(), - dirtyRect.left, dirtyRect.top, dirtyRect.right, dirtyRect.bottom); - } -} diff --git a/core/java/android/view/GLES20TextureLayer.java b/core/java/android/view/GLES20TextureLayer.java deleted file mode 100644 index bb5a6eb..0000000 --- a/core/java/android/view/GLES20TextureLayer.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright (C) 2011 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.view; - -import android.graphics.Canvas; -import android.graphics.Matrix; -import android.graphics.Rect; -import android.graphics.SurfaceTexture; - -/** - * An OpenGL ES 2.0 implementation of {@link HardwareLayer}. This - * implementation can be used as a texture. Rendering into this - * layer is not controlled by a {@link HardwareCanvas}. - */ -class GLES20TextureLayer extends GLES20Layer { - private int mTexture; - private SurfaceTexture mSurface; - - GLES20TextureLayer(boolean isOpaque) { - int[] layerInfo = new int[2]; - mLayer = GLES20Canvas.nCreateTextureLayer(isOpaque, layerInfo); - - if (mLayer != 0) { - mTexture = layerInfo[0]; - mFinalizer = new Finalizer(mLayer); - } else { - mFinalizer = null; - } - } - - @Override - boolean isValid() { - return mLayer != 0 && mTexture != 0; - } - - @Override - boolean resize(int width, int height) { - return isValid(); - } - - @Override - HardwareCanvas getCanvas() { - return null; - } - - @Override - HardwareCanvas start(Canvas currentCanvas) { - return null; - } - - @Override - HardwareCanvas start(Canvas currentCanvas, Rect dirty) { - return null; - } - - @Override - void end(Canvas currentCanvas) { - } - - SurfaceTexture getSurfaceTexture() { - if (mSurface == null) { - mSurface = new SurfaceTexture(mTexture); - } - return mSurface; - } - - void setSurfaceTexture(SurfaceTexture surfaceTexture) { - if (mSurface != null) { - mSurface.release(); - } - mSurface = surfaceTexture; - mSurface.attachToGLContext(mTexture); - } - - @Override - void update(int width, int height, boolean isOpaque) { - super.update(width, height, isOpaque); - GLES20Canvas.nUpdateTextureLayer(mLayer, width, height, isOpaque, mSurface); - } - - @Override - void setOpaque(boolean isOpaque) { - throw new UnsupportedOperationException("Use update(int, int, boolean) instead"); - } - - @Override - void setTransform(Matrix matrix) { - GLES20Canvas.nSetTextureLayerTransform(mLayer, matrix.native_instance); - } - - @Override - void redrawLater(DisplayList displayList, Rect dirtyRect) { - } -} diff --git a/core/java/android/view/GLRenderer.java b/core/java/android/view/GLRenderer.java new file mode 100644 index 0000000..eba4f7f --- /dev/null +++ b/core/java/android/view/GLRenderer.java @@ -0,0 +1,1566 @@ +/* + * 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.view; + +import static javax.microedition.khronos.egl.EGL10.EGL_ALPHA_SIZE; +import static javax.microedition.khronos.egl.EGL10.EGL_BAD_NATIVE_WINDOW; +import static javax.microedition.khronos.egl.EGL10.EGL_BLUE_SIZE; +import static javax.microedition.khronos.egl.EGL10.EGL_CONFIG_CAVEAT; +import static javax.microedition.khronos.egl.EGL10.EGL_DEFAULT_DISPLAY; +import static javax.microedition.khronos.egl.EGL10.EGL_DEPTH_SIZE; +import static javax.microedition.khronos.egl.EGL10.EGL_DRAW; +import static javax.microedition.khronos.egl.EGL10.EGL_GREEN_SIZE; +import static javax.microedition.khronos.egl.EGL10.EGL_HEIGHT; +import static javax.microedition.khronos.egl.EGL10.EGL_NONE; +import static javax.microedition.khronos.egl.EGL10.EGL_NO_CONTEXT; +import static javax.microedition.khronos.egl.EGL10.EGL_NO_DISPLAY; +import static javax.microedition.khronos.egl.EGL10.EGL_NO_SURFACE; +import static javax.microedition.khronos.egl.EGL10.EGL_RED_SIZE; +import static javax.microedition.khronos.egl.EGL10.EGL_RENDERABLE_TYPE; +import static javax.microedition.khronos.egl.EGL10.EGL_SAMPLES; +import static javax.microedition.khronos.egl.EGL10.EGL_SAMPLE_BUFFERS; +import static javax.microedition.khronos.egl.EGL10.EGL_STENCIL_SIZE; +import static javax.microedition.khronos.egl.EGL10.EGL_SUCCESS; +import static javax.microedition.khronos.egl.EGL10.EGL_SURFACE_TYPE; +import static javax.microedition.khronos.egl.EGL10.EGL_WIDTH; +import static javax.microedition.khronos.egl.EGL10.EGL_WINDOW_BIT; + +import android.content.ComponentCallbacks2; +import android.graphics.Bitmap; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.SurfaceTexture; +import android.opengl.EGL14; +import android.opengl.GLUtils; +import android.opengl.ManagedEGLContext; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.os.SystemProperties; +import android.os.Trace; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.Surface.OutOfResourcesException; + +import com.google.android.gles_jni.EGLImpl; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.locks.ReentrantLock; + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGL11; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; +import javax.microedition.khronos.egl.EGLSurface; +import javax.microedition.khronos.opengles.GL; + +/** + * Hardware renderer using OpenGL + * + * @hide + */ +public class GLRenderer extends HardwareRenderer { + static final int SURFACE_STATE_ERROR = 0; + static final int SURFACE_STATE_SUCCESS = 1; + static final int SURFACE_STATE_UPDATED = 2; + + static final int FUNCTOR_PROCESS_DELAY = 4; + + /** + * Number of frames to profile. + */ + private static final int PROFILE_MAX_FRAMES = 128; + + /** + * Number of floats per profiled frame. + */ + private static final int PROFILE_FRAME_DATA_COUNT = 3; + + private static final int PROFILE_DRAW_MARGIN = 0; + private static final int PROFILE_DRAW_WIDTH = 3; + private static final int[] PROFILE_DRAW_COLORS = { 0xcf3e66cc, 0xcfdc3912, 0xcfe69800 }; + private static final int PROFILE_DRAW_CURRENT_FRAME_COLOR = 0xcf5faa4d; + private static final int PROFILE_DRAW_THRESHOLD_COLOR = 0xff5faa4d; + private static final int PROFILE_DRAW_THRESHOLD_STROKE_WIDTH = 2; + private static final int PROFILE_DRAW_DP_PER_MS = 7; + + private static final String[] VISUALIZERS = { + PROFILE_PROPERTY_VISUALIZE_BARS, + PROFILE_PROPERTY_VISUALIZE_LINES + }; + + private static final String[] OVERDRAW = { + OVERDRAW_PROPERTY_SHOW, + }; + private static final int GL_VERSION = 2; + + static EGL10 sEgl; + static EGLDisplay sEglDisplay; + static EGLConfig sEglConfig; + static final Object[] sEglLock = new Object[0]; + int mWidth = -1, mHeight = -1; + + static final ThreadLocal<ManagedEGLContext> sEglContextStorage + = new ThreadLocal<ManagedEGLContext>(); + + EGLContext mEglContext; + Thread mEglThread; + + EGLSurface mEglSurface; + + GL mGl; + HardwareCanvas mCanvas; + + String mName; + + long mFrameCount; + Paint mDebugPaint; + + static boolean sDirtyRegions; + static final boolean sDirtyRegionsRequested; + static { + String dirtyProperty = SystemProperties.get(RENDER_DIRTY_REGIONS_PROPERTY, "true"); + //noinspection PointlessBooleanExpression,ConstantConditions + sDirtyRegions = "true".equalsIgnoreCase(dirtyProperty); + sDirtyRegionsRequested = sDirtyRegions; + } + + boolean mDirtyRegionsEnabled; + boolean mUpdateDirtyRegions; + + boolean mProfileEnabled; + int mProfileVisualizerType = -1; + float[] mProfileData; + ReentrantLock mProfileLock; + int mProfileCurrentFrame = -PROFILE_FRAME_DATA_COUNT; + + GraphDataProvider mDebugDataProvider; + float[][] mProfileShapes; + Paint mProfilePaint; + + boolean mDebugDirtyRegions; + int mDebugOverdraw = -1; + + final boolean mTranslucent; + + private boolean mDestroyed; + + private final Rect mRedrawClip = new Rect(); + + private final int[] mSurfaceSize = new int[2]; + private final FunctorsRunnable mFunctorsRunnable = new FunctorsRunnable(); + + private long mDrawDelta = Long.MAX_VALUE; + + private GLES20Canvas mGlCanvas; + + private DisplayMetrics mDisplayMetrics; + + private static EGLSurface sPbuffer; + private static final Object[] sPbufferLock = new Object[0]; + + private List<HardwareLayer> mAttachedLayers = new ArrayList<HardwareLayer>(); + + private static class GLRendererEglContext extends ManagedEGLContext { + final Handler mHandler = new Handler(); + + public GLRendererEglContext(EGLContext context) { + super(context); + } + + @Override + public void onTerminate(final EGLContext eglContext) { + // Make sure we do this on the correct thread. + if (mHandler.getLooper() != Looper.myLooper()) { + mHandler.post(new Runnable() { + @Override + public void run() { + onTerminate(eglContext); + } + }); + return; + } + + synchronized (sEglLock) { + if (sEgl == null) return; + + if (EGLImpl.getInitCount(sEglDisplay) == 1) { + usePbufferSurface(eglContext); + GLES20Canvas.terminateCaches(); + + sEgl.eglDestroyContext(sEglDisplay, eglContext); + sEglContextStorage.set(null); + sEglContextStorage.remove(); + + sEgl.eglDestroySurface(sEglDisplay, sPbuffer); + sEgl.eglMakeCurrent(sEglDisplay, EGL_NO_SURFACE, + EGL_NO_SURFACE, EGL_NO_CONTEXT); + + sEgl.eglReleaseThread(); + sEgl.eglTerminate(sEglDisplay); + + sEgl = null; + sEglDisplay = null; + sEglConfig = null; + sPbuffer = null; + } + } + } + } + + HardwareCanvas createCanvas() { + return mGlCanvas = new GLES20Canvas(mTranslucent); + } + + ManagedEGLContext createManagedContext(EGLContext eglContext) { + return new GLRendererEglContext(mEglContext); + } + + int[] getConfig(boolean dirtyRegions) { + //noinspection PointlessBooleanExpression,ConstantConditions + final int stencilSize = GLES20Canvas.getStencilSize(); + final int swapBehavior = dirtyRegions ? EGL14.EGL_SWAP_BEHAVIOR_PRESERVED_BIT : 0; + + return new int[] { + EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_ALPHA_SIZE, 8, + EGL_DEPTH_SIZE, 0, + EGL_CONFIG_CAVEAT, EGL_NONE, + EGL_STENCIL_SIZE, stencilSize, + EGL_SURFACE_TYPE, EGL_WINDOW_BIT | swapBehavior, + EGL_NONE + }; + } + + void initCaches() { + if (GLES20Canvas.initCaches()) { + // Caches were (re)initialized, rebind atlas + initAtlas(); + } + } + + void initAtlas() { + IBinder binder = ServiceManager.getService("assetatlas"); + if (binder == null) return; + + IAssetAtlas atlas = IAssetAtlas.Stub.asInterface(binder); + try { + if (atlas.isCompatible(android.os.Process.myPpid())) { + GraphicBuffer buffer = atlas.getBuffer(); + if (buffer != null) { + long[] map = atlas.getMap(); + if (map != null) { + GLES20Canvas.initAtlas(buffer, map); + } + // If IAssetAtlas is not the same class as the IBinder + // we are using a remote service and we can safely + // destroy the graphic buffer + if (atlas.getClass() != binder.getClass()) { + buffer.destroy(); + } + } + } + } catch (RemoteException e) { + Log.w(LOG_TAG, "Could not acquire atlas", e); + } + } + + boolean canDraw() { + return mGl != null && mCanvas != null && mGlCanvas != null; + } + + int onPreDraw(Rect dirty) { + return mGlCanvas.onPreDraw(dirty); + } + + void onPostDraw() { + mGlCanvas.onPostDraw(); + } + + void drawProfileData(View.AttachInfo attachInfo) { + if (mDebugDataProvider != null) { + final GraphDataProvider provider = mDebugDataProvider; + initProfileDrawData(attachInfo, provider); + + final int height = provider.getVerticalUnitSize(); + final int margin = provider.getHorizontaUnitMargin(); + final int width = provider.getHorizontalUnitSize(); + + int x = 0; + int count = 0; + int current = 0; + + final float[] data = provider.getData(); + final int elementCount = provider.getElementCount(); + final int graphType = provider.getGraphType(); + + int totalCount = provider.getFrameCount() * elementCount; + if (graphType == GraphDataProvider.GRAPH_TYPE_LINES) { + totalCount -= elementCount; + } + + for (int i = 0; i < totalCount; i += elementCount) { + if (data[i] < 0.0f) break; + + int index = count * 4; + if (i == provider.getCurrentFrame() * elementCount) current = index; + + x += margin; + int x2 = x + width; + + int y2 = mHeight; + int y1 = (int) (y2 - data[i] * height); + + switch (graphType) { + case GraphDataProvider.GRAPH_TYPE_BARS: { + for (int j = 0; j < elementCount; j++) { + //noinspection MismatchedReadAndWriteOfArray + final float[] r = mProfileShapes[j]; + r[index] = x; + r[index + 1] = y1; + r[index + 2] = x2; + r[index + 3] = y2; + + y2 = y1; + if (j < elementCount - 1) { + y1 = (int) (y2 - data[i + j + 1] * height); + } + } + } break; + case GraphDataProvider.GRAPH_TYPE_LINES: { + for (int j = 0; j < elementCount; j++) { + //noinspection MismatchedReadAndWriteOfArray + final float[] r = mProfileShapes[j]; + r[index] = (x + x2) * 0.5f; + r[index + 1] = index == 0 ? y1 : r[index - 1]; + r[index + 2] = r[index] + width; + r[index + 3] = y1; + + y2 = y1; + if (j < elementCount - 1) { + y1 = (int) (y2 - data[i + j + 1] * height); + } + } + } break; + } + + + x += width; + count++; + } + + x += margin; + + drawGraph(graphType, count); + drawCurrentFrame(graphType, current); + drawThreshold(x, height); + } + } + + private void drawGraph(int graphType, int count) { + for (int i = 0; i < mProfileShapes.length; i++) { + mDebugDataProvider.setupGraphPaint(mProfilePaint, i); + switch (graphType) { + case GraphDataProvider.GRAPH_TYPE_BARS: + mGlCanvas.drawRects(mProfileShapes[i], count * 4, mProfilePaint); + break; + case GraphDataProvider.GRAPH_TYPE_LINES: + mGlCanvas.drawLines(mProfileShapes[i], 0, count * 4, mProfilePaint); + break; + } + } + } + + private void drawCurrentFrame(int graphType, int index) { + if (index >= 0) { + mDebugDataProvider.setupCurrentFramePaint(mProfilePaint); + switch (graphType) { + case GraphDataProvider.GRAPH_TYPE_BARS: + mGlCanvas.drawRect(mProfileShapes[2][index], mProfileShapes[2][index + 1], + mProfileShapes[2][index + 2], mProfileShapes[0][index + 3], + mProfilePaint); + break; + case GraphDataProvider.GRAPH_TYPE_LINES: + mGlCanvas.drawLine(mProfileShapes[2][index], mProfileShapes[2][index + 1], + mProfileShapes[2][index], mHeight, mProfilePaint); + break; + } + } + } + + private void drawThreshold(int x, int height) { + float threshold = mDebugDataProvider.getThreshold(); + if (threshold > 0.0f) { + mDebugDataProvider.setupThresholdPaint(mProfilePaint); + int y = (int) (mHeight - threshold * height); + mGlCanvas.drawLine(0.0f, y, x, y, mProfilePaint); + } + } + + private void initProfileDrawData(View.AttachInfo attachInfo, GraphDataProvider provider) { + if (mProfileShapes == null) { + final int elementCount = provider.getElementCount(); + final int frameCount = provider.getFrameCount(); + + mProfileShapes = new float[elementCount][]; + for (int i = 0; i < elementCount; i++) { + mProfileShapes[i] = new float[frameCount * 4]; + } + + mProfilePaint = new Paint(); + } + + mProfilePaint.reset(); + if (provider.getGraphType() == GraphDataProvider.GRAPH_TYPE_LINES) { + mProfilePaint.setAntiAlias(true); + } + + if (mDisplayMetrics == null) { + mDisplayMetrics = new DisplayMetrics(); + } + + attachInfo.mDisplay.getMetrics(mDisplayMetrics); + provider.prepare(mDisplayMetrics); + } + + @Override + void destroy(boolean full) { + try { + if (full && mCanvas != null) { + mCanvas = null; + } + + if (!isEnabled() || mDestroyed) { + setEnabled(false); + return; + } + + destroySurface(); + setEnabled(false); + + mDestroyed = true; + mGl = null; + } finally { + if (full && mGlCanvas != null) { + mGlCanvas = null; + } + } + } + + @Override + void pushLayerUpdate(HardwareLayer layer) { + mGlCanvas.pushLayerUpdate(layer); + } + + @Override + void flushLayerUpdates() { + if (validate()) { + flushLayerChanges(); + mGlCanvas.flushLayerUpdates(); + } + } + + @Override + HardwareLayer createTextureLayer() { + validate(); + return HardwareLayer.createTextureLayer(this); + } + + @Override + public HardwareLayer createDisplayListLayer(int width, int height) { + validate(); + return HardwareLayer.createDisplayListLayer(this, width, height); + } + + @Override + void onLayerCreated(HardwareLayer hardwareLayer) { + mAttachedLayers.add(hardwareLayer); + } + + boolean hasContext() { + return sEgl != null && mEglContext != null + && mEglContext.equals(sEgl.eglGetCurrentContext()); + } + + @Override + void onLayerDestroyed(HardwareLayer layer) { + if (mGlCanvas != null) { + mGlCanvas.cancelLayerUpdate(layer); + } + if (hasContext()) { + long backingLayer = layer.detachBackingLayer(); + nDestroyLayer(backingLayer); + } + mAttachedLayers.remove(layer); + } + + @Override + public SurfaceTexture createSurfaceTexture(HardwareLayer layer) { + return layer.createSurfaceTexture(); + } + + @Override + boolean copyLayerInto(HardwareLayer layer, Bitmap bitmap) { + if (!validate()) { + throw new IllegalStateException("Could not acquire hardware rendering context"); + } + layer.flushChanges(); + return GLES20Canvas.nCopyLayer(layer.getLayer(), bitmap.mNativeBitmap); + } + + @Override + boolean safelyRun(Runnable action) { + boolean needsContext = !isEnabled() || checkRenderContext() == SURFACE_STATE_ERROR; + + if (needsContext) { + GLRendererEglContext managedContext = + (GLRendererEglContext) sEglContextStorage.get(); + if (managedContext == null) return false; + usePbufferSurface(managedContext.getContext()); + } + + try { + action.run(); + } finally { + if (needsContext) { + sEgl.eglMakeCurrent(sEglDisplay, EGL_NO_SURFACE, + EGL_NO_SURFACE, EGL_NO_CONTEXT); + } + } + + return true; + } + + @Override + public void invokeFunctor(long functor, boolean waitForCompletion) { + boolean needsContext = !isEnabled() || checkRenderContext() == SURFACE_STATE_ERROR; + boolean hasContext = !needsContext; + + if (needsContext) { + GLRendererEglContext managedContext = + (GLRendererEglContext) sEglContextStorage.get(); + if (managedContext != null) { + usePbufferSurface(managedContext.getContext()); + hasContext = true; + } + } + + try { + nInvokeFunctor(functor, hasContext); + } finally { + if (needsContext) { + sEgl.eglMakeCurrent(sEglDisplay, EGL_NO_SURFACE, + EGL_NO_SURFACE, EGL_NO_CONTEXT); + } + } + } + + private static native void nInvokeFunctor(long functor, boolean hasContext); + + @Override + void destroyHardwareResources(final View view) { + if (view != null) { + safelyRun(new Runnable() { + @Override + public void run() { + if (mCanvas != null) { + mCanvas.clearLayerUpdates(); + } + destroyResources(view); + GLES20Canvas.flushCaches(GLES20Canvas.FLUSH_CACHES_LAYERS); + } + }); + } + } + + private static void destroyResources(View view) { + view.destroyHardwareResources(); + + if (view instanceof ViewGroup) { + ViewGroup group = (ViewGroup) view; + + int count = group.getChildCount(); + for (int i = 0; i < count; i++) { + destroyResources(group.getChildAt(i)); + } + } + } + + static void startTrimMemory(int level) { + if (sEgl == null || sEglConfig == null) return; + + GLRendererEglContext managedContext = + (GLRendererEglContext) sEglContextStorage.get(); + // We do not have OpenGL objects + if (managedContext == null) { + return; + } else { + usePbufferSurface(managedContext.getContext()); + } + + if (level >= ComponentCallbacks2.TRIM_MEMORY_COMPLETE) { + GLES20Canvas.flushCaches(GLES20Canvas.FLUSH_CACHES_FULL); + } else if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) { + GLES20Canvas.flushCaches(GLES20Canvas.FLUSH_CACHES_MODERATE); + } + } + + static void endTrimMemory() { + if (sEgl != null && sEglDisplay != null) { + sEgl.eglMakeCurrent(sEglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + } + } + + private static void usePbufferSurface(EGLContext eglContext) { + synchronized (sPbufferLock) { + // Create a temporary 1x1 pbuffer so we have a context + // to clear our OpenGL objects + if (sPbuffer == null) { + sPbuffer = sEgl.eglCreatePbufferSurface(sEglDisplay, sEglConfig, new int[] { + EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE + }); + } + } + sEgl.eglMakeCurrent(sEglDisplay, sPbuffer, sPbuffer, eglContext); + } + + GLRenderer(boolean translucent) { + mTranslucent = translucent; + + loadSystemProperties(); + } + + @Override + boolean loadSystemProperties() { + boolean value; + boolean changed = false; + + String profiling = SystemProperties.get(PROFILE_PROPERTY); + int graphType = search(VISUALIZERS, profiling); + value = graphType >= 0; + + if (graphType != mProfileVisualizerType) { + changed = true; + mProfileVisualizerType = graphType; + + mProfileShapes = null; + mProfilePaint = null; + + if (value) { + mDebugDataProvider = new DrawPerformanceDataProvider(graphType); + } else { + mDebugDataProvider = null; + } + } + + // If on-screen profiling is not enabled, we need to check whether + // console profiling only is enabled + if (!value) { + value = Boolean.parseBoolean(profiling); + } + + if (value != mProfileEnabled) { + changed = true; + mProfileEnabled = value; + + if (mProfileEnabled) { + Log.d(LOG_TAG, "Profiling hardware renderer"); + + int maxProfileFrames = SystemProperties.getInt(PROFILE_MAXFRAMES_PROPERTY, + PROFILE_MAX_FRAMES); + mProfileData = new float[maxProfileFrames * PROFILE_FRAME_DATA_COUNT]; + for (int i = 0; i < mProfileData.length; i += PROFILE_FRAME_DATA_COUNT) { + mProfileData[i] = mProfileData[i + 1] = mProfileData[i + 2] = -1; + } + + mProfileLock = new ReentrantLock(); + } else { + mProfileData = null; + mProfileLock = null; + mProfileVisualizerType = -1; + } + + mProfileCurrentFrame = -PROFILE_FRAME_DATA_COUNT; + } + + value = SystemProperties.getBoolean(DEBUG_DIRTY_REGIONS_PROPERTY, false); + if (value != mDebugDirtyRegions) { + changed = true; + mDebugDirtyRegions = value; + + if (mDebugDirtyRegions) { + Log.d(LOG_TAG, "Debugging dirty regions"); + } + } + + String overdraw = SystemProperties.get(HardwareRenderer.DEBUG_OVERDRAW_PROPERTY); + int debugOverdraw = search(OVERDRAW, overdraw); + if (debugOverdraw != mDebugOverdraw) { + changed = true; + mDebugOverdraw = debugOverdraw; + } + + if (loadProperties()) { + changed = true; + } + + return changed; + } + + private static int search(String[] values, String value) { + for (int i = 0; i < values.length; i++) { + if (values[i].equals(value)) return i; + } + return -1; + } + + @Override + void dumpGfxInfo(PrintWriter pw) { + if (mProfileEnabled) { + pw.printf("\n\tDraw\tProcess\tExecute\n"); + + mProfileLock.lock(); + try { + for (int i = 0; i < mProfileData.length; i += PROFILE_FRAME_DATA_COUNT) { + if (mProfileData[i] < 0) { + break; + } + pw.printf("\t%3.2f\t%3.2f\t%3.2f\n", mProfileData[i], mProfileData[i + 1], + mProfileData[i + 2]); + mProfileData[i] = mProfileData[i + 1] = mProfileData[i + 2] = -1; + } + mProfileCurrentFrame = mProfileData.length; + } finally { + mProfileLock.unlock(); + } + } + } + + @Override + long getFrameCount() { + return mFrameCount; + } + + /** + * Indicates whether this renderer instance can track and update dirty regions. + */ + boolean hasDirtyRegions() { + return mDirtyRegionsEnabled; + } + + /** + * Checks for OpenGL errors. If an error has occured, {@link #destroy(boolean)} + * is invoked and the requested flag is turned off. The error code is + * also logged as a warning. + */ + void checkEglErrors() { + if (isEnabled()) { + checkEglErrorsForced(); + } + } + + private void checkEglErrorsForced() { + int error = sEgl.eglGetError(); + if (error != EGL_SUCCESS) { + // something bad has happened revert to + // normal rendering. + Log.w(LOG_TAG, "EGL error: " + GLUtils.getEGLErrorString(error)); + fallback(error != EGL11.EGL_CONTEXT_LOST); + } + } + + private void fallback(boolean fallback) { + destroy(true); + if (fallback) { + // we'll try again if it was context lost + setRequested(false); + Log.w(LOG_TAG, "Mountain View, we've had a problem here. " + + "Switching back to software rendering."); + } + } + + @Override + boolean initialize(Surface surface) throws OutOfResourcesException { + if (isRequested() && !isEnabled()) { + boolean contextCreated = initializeEgl(); + mGl = createEglSurface(surface); + mDestroyed = false; + + if (mGl != null) { + int err = sEgl.eglGetError(); + if (err != EGL_SUCCESS) { + destroy(true); + setRequested(false); + } else { + if (mCanvas == null) { + mCanvas = createCanvas(); + } + setEnabled(true); + + if (contextCreated) { + initAtlas(); + } + } + + return mCanvas != null; + } + } + return false; + } + + @Override + void updateSurface(Surface surface) throws OutOfResourcesException { + if (isRequested() && isEnabled()) { + createEglSurface(surface); + } + } + + @Override + void pauseSurface(Surface surface) { + // No-op + } + + boolean initializeEgl() { + synchronized (sEglLock) { + if (sEgl == null && sEglConfig == null) { + sEgl = (EGL10) EGLContext.getEGL(); + + // Get to the default display. + sEglDisplay = sEgl.eglGetDisplay(EGL_DEFAULT_DISPLAY); + + if (sEglDisplay == EGL_NO_DISPLAY) { + throw new RuntimeException("eglGetDisplay failed " + + GLUtils.getEGLErrorString(sEgl.eglGetError())); + } + + // We can now initialize EGL for that display + int[] version = new int[2]; + if (!sEgl.eglInitialize(sEglDisplay, version)) { + throw new RuntimeException("eglInitialize failed " + + GLUtils.getEGLErrorString(sEgl.eglGetError())); + } + + checkEglErrorsForced(); + + sEglConfig = loadEglConfig(); + } + } + + ManagedEGLContext managedContext = sEglContextStorage.get(); + mEglContext = managedContext != null ? managedContext.getContext() : null; + mEglThread = Thread.currentThread(); + + if (mEglContext == null) { + mEglContext = createContext(sEgl, sEglDisplay, sEglConfig); + sEglContextStorage.set(createManagedContext(mEglContext)); + return true; + } + + return false; + } + + private EGLConfig loadEglConfig() { + EGLConfig eglConfig = chooseEglConfig(); + if (eglConfig == null) { + // We tried to use EGL_SWAP_BEHAVIOR_PRESERVED_BIT, try again without + if (sDirtyRegions) { + sDirtyRegions = false; + eglConfig = chooseEglConfig(); + if (eglConfig == null) { + throw new RuntimeException("eglConfig not initialized"); + } + } else { + throw new RuntimeException("eglConfig not initialized"); + } + } + return eglConfig; + } + + private EGLConfig chooseEglConfig() { + EGLConfig[] configs = new EGLConfig[1]; + int[] configsCount = new int[1]; + int[] configSpec = getConfig(sDirtyRegions); + + // Debug + final String debug = SystemProperties.get(PRINT_CONFIG_PROPERTY, ""); + if ("all".equalsIgnoreCase(debug)) { + sEgl.eglChooseConfig(sEglDisplay, configSpec, null, 0, configsCount); + + EGLConfig[] debugConfigs = new EGLConfig[configsCount[0]]; + sEgl.eglChooseConfig(sEglDisplay, configSpec, debugConfigs, + configsCount[0], configsCount); + + for (EGLConfig config : debugConfigs) { + printConfig(config); + } + } + + if (!sEgl.eglChooseConfig(sEglDisplay, configSpec, configs, 1, configsCount)) { + throw new IllegalArgumentException("eglChooseConfig failed " + + GLUtils.getEGLErrorString(sEgl.eglGetError())); + } else if (configsCount[0] > 0) { + if ("choice".equalsIgnoreCase(debug)) { + printConfig(configs[0]); + } + return configs[0]; + } + + return null; + } + + private static void printConfig(EGLConfig config) { + int[] value = new int[1]; + + Log.d(LOG_TAG, "EGL configuration " + config + ":"); + + sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_RED_SIZE, value); + Log.d(LOG_TAG, " RED_SIZE = " + value[0]); + + sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_GREEN_SIZE, value); + Log.d(LOG_TAG, " GREEN_SIZE = " + value[0]); + + sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_BLUE_SIZE, value); + Log.d(LOG_TAG, " BLUE_SIZE = " + value[0]); + + sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_ALPHA_SIZE, value); + Log.d(LOG_TAG, " ALPHA_SIZE = " + value[0]); + + sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_DEPTH_SIZE, value); + Log.d(LOG_TAG, " DEPTH_SIZE = " + value[0]); + + sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_STENCIL_SIZE, value); + Log.d(LOG_TAG, " STENCIL_SIZE = " + value[0]); + + sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_SAMPLE_BUFFERS, value); + Log.d(LOG_TAG, " SAMPLE_BUFFERS = " + value[0]); + + sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_SAMPLES, value); + Log.d(LOG_TAG, " SAMPLES = " + value[0]); + + sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_SURFACE_TYPE, value); + Log.d(LOG_TAG, " SURFACE_TYPE = 0x" + Integer.toHexString(value[0])); + + sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_CONFIG_CAVEAT, value); + Log.d(LOG_TAG, " CONFIG_CAVEAT = 0x" + Integer.toHexString(value[0])); + } + + GL createEglSurface(Surface surface) throws OutOfResourcesException { + // Check preconditions. + if (sEgl == null) { + throw new RuntimeException("egl not initialized"); + } + if (sEglDisplay == null) { + throw new RuntimeException("eglDisplay not initialized"); + } + if (sEglConfig == null) { + throw new RuntimeException("eglConfig not initialized"); + } + if (Thread.currentThread() != mEglThread) { + throw new IllegalStateException("HardwareRenderer cannot be used " + + "from multiple threads"); + } + + // In case we need to destroy an existing surface + destroySurface(); + + // Create an EGL surface we can render into. + if (!createSurface(surface)) { + return null; + } + + initCaches(); + + return mEglContext.getGL(); + } + + private void enableDirtyRegions() { + // If mDirtyRegions is set, this means we have an EGL configuration + // with EGL_SWAP_BEHAVIOR_PRESERVED_BIT set + if (sDirtyRegions) { + if (!(mDirtyRegionsEnabled = preserveBackBuffer())) { + Log.w(LOG_TAG, "Backbuffer cannot be preserved"); + } + } else if (sDirtyRegionsRequested) { + // If mDirtyRegions is not set, our EGL configuration does not + // have EGL_SWAP_BEHAVIOR_PRESERVED_BIT; however, the default + // swap behavior might be EGL_BUFFER_PRESERVED, which means we + // want to set mDirtyRegions. We try to do this only if dirty + // regions were initially requested as part of the device + // configuration (see RENDER_DIRTY_REGIONS) + mDirtyRegionsEnabled = isBackBufferPreserved(); + } + } + + EGLContext createContext(EGL10 egl, EGLDisplay eglDisplay, EGLConfig eglConfig) { + final int[] attribs = { EGL14.EGL_CONTEXT_CLIENT_VERSION, GL_VERSION, EGL_NONE }; + + EGLContext context = egl.eglCreateContext(eglDisplay, eglConfig, EGL_NO_CONTEXT, + attribs); + if (context == null || context == EGL_NO_CONTEXT) { + //noinspection ConstantConditions + throw new IllegalStateException( + "Could not create an EGL context. eglCreateContext failed with error: " + + GLUtils.getEGLErrorString(sEgl.eglGetError())); + } + + return context; + } + + void destroySurface() { + if (mEglSurface != null && mEglSurface != EGL_NO_SURFACE) { + if (mEglSurface.equals(sEgl.eglGetCurrentSurface(EGL_DRAW))) { + sEgl.eglMakeCurrent(sEglDisplay, + EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + } + sEgl.eglDestroySurface(sEglDisplay, mEglSurface); + mEglSurface = null; + } + } + + @Override + void invalidate(Surface surface) { + // Cancels any existing buffer to ensure we'll get a buffer + // of the right size before we call eglSwapBuffers + sEgl.eglMakeCurrent(sEglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + + if (mEglSurface != null && mEglSurface != EGL_NO_SURFACE) { + sEgl.eglDestroySurface(sEglDisplay, mEglSurface); + mEglSurface = null; + setEnabled(false); + } + + if (surface.isValid()) { + if (!createSurface(surface)) { + return; + } + + mUpdateDirtyRegions = true; + + if (mCanvas != null) { + setEnabled(true); + } + } + } + + private boolean createSurface(Surface surface) { + mEglSurface = sEgl.eglCreateWindowSurface(sEglDisplay, sEglConfig, surface, null); + + if (mEglSurface == null || mEglSurface == EGL_NO_SURFACE) { + int error = sEgl.eglGetError(); + if (error == EGL_BAD_NATIVE_WINDOW) { + Log.e(LOG_TAG, "createWindowSurface returned EGL_BAD_NATIVE_WINDOW."); + return false; + } + throw new RuntimeException("createWindowSurface failed " + + GLUtils.getEGLErrorString(error)); + } + + if (!sEgl.eglMakeCurrent(sEglDisplay, mEglSurface, mEglSurface, mEglContext)) { + throw new IllegalStateException("eglMakeCurrent failed " + + GLUtils.getEGLErrorString(sEgl.eglGetError())); + } + + enableDirtyRegions(); + + return true; + } + + boolean validate() { + return checkRenderContext() != SURFACE_STATE_ERROR; + } + + @Override + void setup(int width, int height) { + if (validate()) { + mCanvas.setViewport(width, height); + mWidth = width; + mHeight = height; + } + } + + @Override + int getWidth() { + return mWidth; + } + + @Override + int getHeight() { + return mHeight; + } + + @Override + void setName(String name) { + mName = name; + } + + class FunctorsRunnable implements Runnable { + View.AttachInfo attachInfo; + + @Override + public void run() { + final HardwareRenderer renderer = attachInfo.mHardwareRenderer; + if (renderer == null || !renderer.isEnabled() || renderer != GLRenderer.this) { + return; + } + + if (checkRenderContext() != SURFACE_STATE_ERROR) { + mCanvas.invokeFunctors(mRedrawClip); + } + } + } + + @Override + void draw(View view, View.AttachInfo attachInfo, HardwareDrawCallbacks callbacks, + Rect dirty) { + if (canDraw()) { + if (!hasDirtyRegions()) { + dirty = null; + } + attachInfo.mIgnoreDirtyState = true; + attachInfo.mDrawingTime = SystemClock.uptimeMillis(); + + view.mPrivateFlags |= View.PFLAG_DRAWN; + + // We are already on the correct thread + final int surfaceState = checkRenderContextUnsafe(); + if (surfaceState != SURFACE_STATE_ERROR) { + HardwareCanvas canvas = mCanvas; + + if (mProfileEnabled) { + mProfileLock.lock(); + } + + dirty = beginFrame(canvas, dirty, surfaceState); + + RenderNode displayList = buildDisplayList(view, canvas); + + flushLayerChanges(); + + // buildDisplayList() calls into user code which can cause + // an eglMakeCurrent to happen with a different surface/context. + // We must therefore check again here. + if (checkRenderContextUnsafe() == SURFACE_STATE_ERROR) { + return; + } + + int saveCount = 0; + int status = RenderNode.STATUS_DONE; + + long start = getSystemTime(); + try { + status = prepareFrame(dirty); + + saveCount = canvas.save(); + callbacks.onHardwarePreDraw(canvas); + + if (displayList != null) { + status |= drawDisplayList(attachInfo, canvas, displayList, status); + } else { + // Shouldn't reach here + view.draw(canvas); + } + } catch (Exception e) { + Log.e(LOG_TAG, "An error has occurred while drawing:", e); + } finally { + callbacks.onHardwarePostDraw(canvas); + canvas.restoreToCount(saveCount); + view.mRecreateDisplayList = false; + + mDrawDelta = getSystemTime() - start; + + if (mDrawDelta > 0) { + mFrameCount++; + + debugDirtyRegions(dirty, canvas); + drawProfileData(attachInfo); + } + } + + onPostDraw(); + + swapBuffers(status); + + if (mProfileEnabled) { + mProfileLock.unlock(); + } + + attachInfo.mIgnoreDirtyState = false; + } + } + } + + private void flushLayerChanges() { + // Loop through and apply any pending layer changes + for (int i = 0; i < mAttachedLayers.size(); i++) { + HardwareLayer layer = mAttachedLayers.get(i); + layer.flushChanges(); + if (!layer.isValid()) { + // The layer was removed from mAttachedLayers, rewind i by 1 + // Note that this shouldn't actually happen as View.getHardwareLayer() + // is already flushing for error checking reasons + i--; + } + } + } + + @Override + void fence() { + // Everything is immediate, so this is a no-op + } + + private RenderNode buildDisplayList(View view, HardwareCanvas canvas) { + if (mDrawDelta <= 0) { + return view.mRenderNode; + } + + view.mRecreateDisplayList = (view.mPrivateFlags & View.PFLAG_INVALIDATED) + == View.PFLAG_INVALIDATED; + view.mPrivateFlags &= ~View.PFLAG_INVALIDATED; + + long buildDisplayListStartTime = startBuildDisplayListProfiling(); + canvas.clearLayerUpdates(); + + Trace.traceBegin(Trace.TRACE_TAG_VIEW, "getDisplayList"); + RenderNode renderNode = view.getDisplayList(); + Trace.traceEnd(Trace.TRACE_TAG_VIEW); + + endBuildDisplayListProfiling(buildDisplayListStartTime); + + return renderNode; + } + + private Rect beginFrame(HardwareCanvas canvas, Rect dirty, int surfaceState) { + // We had to change the current surface and/or context, redraw everything + if (surfaceState == SURFACE_STATE_UPDATED) { + dirty = null; + beginFrame(null); + } else { + int[] size = mSurfaceSize; + beginFrame(size); + + if (size[1] != mHeight || size[0] != mWidth) { + mWidth = size[0]; + mHeight = size[1]; + + canvas.setViewport(mWidth, mHeight); + + dirty = null; + } + } + + if (mDebugDataProvider != null) dirty = null; + + return dirty; + } + + private long startBuildDisplayListProfiling() { + if (mProfileEnabled) { + mProfileCurrentFrame += PROFILE_FRAME_DATA_COUNT; + if (mProfileCurrentFrame >= mProfileData.length) { + mProfileCurrentFrame = 0; + } + + return System.nanoTime(); + } + return 0; + } + + private void endBuildDisplayListProfiling(long getDisplayListStartTime) { + if (mProfileEnabled) { + long now = System.nanoTime(); + float total = (now - getDisplayListStartTime) * 0.000001f; + //noinspection PointlessArithmeticExpression + mProfileData[mProfileCurrentFrame] = total; + } + } + + private int prepareFrame(Rect dirty) { + int status; + Trace.traceBegin(Trace.TRACE_TAG_VIEW, "prepareFrame"); + try { + status = onPreDraw(dirty); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIEW); + } + return status; + } + + private int drawDisplayList(View.AttachInfo attachInfo, HardwareCanvas canvas, + RenderNode displayList, int status) { + + long drawDisplayListStartTime = 0; + if (mProfileEnabled) { + drawDisplayListStartTime = System.nanoTime(); + } + + Trace.traceBegin(Trace.TRACE_TAG_VIEW, "drawDisplayList"); + nPrepareTree(displayList.getNativeDisplayList()); + try { + status |= canvas.drawDisplayList(displayList, mRedrawClip, + RenderNode.FLAG_CLIP_CHILDREN); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIEW); + } + + if (mProfileEnabled) { + long now = System.nanoTime(); + float total = (now - drawDisplayListStartTime) * 0.000001f; + mProfileData[mProfileCurrentFrame + 1] = total; + } + + return status; + } + + private void swapBuffers(int status) { + if ((status & RenderNode.STATUS_DREW) == RenderNode.STATUS_DREW) { + long eglSwapBuffersStartTime = 0; + if (mProfileEnabled) { + eglSwapBuffersStartTime = System.nanoTime(); + } + + sEgl.eglSwapBuffers(sEglDisplay, mEglSurface); + + if (mProfileEnabled) { + long now = System.nanoTime(); + float total = (now - eglSwapBuffersStartTime) * 0.000001f; + mProfileData[mProfileCurrentFrame + 2] = total; + } + + checkEglErrors(); + } + } + + private void debugDirtyRegions(Rect dirty, HardwareCanvas canvas) { + if (mDebugDirtyRegions) { + if (mDebugPaint == null) { + mDebugPaint = new Paint(); + mDebugPaint.setColor(0x7fff0000); + } + + if (dirty != null && (mFrameCount & 1) == 0) { + canvas.drawRect(dirty, mDebugPaint); + } + } + } + + @Override + void detachFunctor(long functor) { + if (mCanvas != null) { + mCanvas.detachFunctor(functor); + } + } + + @Override + void attachFunctor(View.AttachInfo attachInfo, long functor) { + if (mCanvas != null) { + mCanvas.attachFunctor(functor); + mFunctorsRunnable.attachInfo = attachInfo; + attachInfo.mHandler.removeCallbacks(mFunctorsRunnable); + attachInfo.mHandler.postDelayed(mFunctorsRunnable, 0); + } + } + + /** + * Ensures the current EGL context and surface are the ones we expect. + * This method throws an IllegalStateException if invoked from a thread + * that did not initialize EGL. + * + * @return {@link #SURFACE_STATE_ERROR} if the correct EGL context cannot be made current, + * {@link #SURFACE_STATE_UPDATED} if the EGL context was changed or + * {@link #SURFACE_STATE_SUCCESS} if the EGL context was the correct one + * + * @see #checkRenderContextUnsafe() + */ + int checkRenderContext() { + if (mEglThread != Thread.currentThread()) { + throw new IllegalStateException("Hardware acceleration can only be used with a " + + "single UI thread.\nOriginal thread: " + mEglThread + "\n" + + "Current thread: " + Thread.currentThread()); + } + + return checkRenderContextUnsafe(); + } + + /** + * Ensures the current EGL context and surface are the ones we expect. + * This method does not check the current thread. + * + * @return {@link #SURFACE_STATE_ERROR} if the correct EGL context cannot be made current, + * {@link #SURFACE_STATE_UPDATED} if the EGL context was changed or + * {@link #SURFACE_STATE_SUCCESS} if the EGL context was the correct one + * + * @see #checkRenderContext() + */ + private int checkRenderContextUnsafe() { + if (!mEglSurface.equals(sEgl.eglGetCurrentSurface(EGL_DRAW)) || + !mEglContext.equals(sEgl.eglGetCurrentContext())) { + if (!sEgl.eglMakeCurrent(sEglDisplay, mEglSurface, mEglSurface, mEglContext)) { + Log.e(LOG_TAG, "eglMakeCurrent failed " + + GLUtils.getEGLErrorString(sEgl.eglGetError())); + fallback(true); + return SURFACE_STATE_ERROR; + } else { + if (mUpdateDirtyRegions) { + enableDirtyRegions(); + mUpdateDirtyRegions = false; + } + return SURFACE_STATE_UPDATED; + } + } + return SURFACE_STATE_SUCCESS; + } + + private static int dpToPx(int dp, float density) { + return (int) (dp * density + 0.5f); + } + + static native boolean loadProperties(); + + static native void setupShadersDiskCache(String cacheFile); + + /** + * Notifies EGL that the frame is about to be rendered. + * @param size + */ + static native void beginFrame(int[] size); + + /** + * Returns the current system time according to the renderer. + * This method is used for debugging only and should not be used + * as a clock. + */ + static native long getSystemTime(); + + /** + * Preserves the back buffer of the current surface after a buffer swap. + * Calling this method sets the EGL_SWAP_BEHAVIOR attribute of the current + * surface to EGL_BUFFER_PRESERVED. Calling this method requires an EGL + * config that supports EGL_SWAP_BEHAVIOR_PRESERVED_BIT. + * + * @return True if the swap behavior was successfully changed, + * false otherwise. + */ + static native boolean preserveBackBuffer(); + + /** + * Indicates whether the current surface preserves its back buffer + * after a buffer swap. + * + * @return True, if the surface's EGL_SWAP_BEHAVIOR is EGL_BUFFER_PRESERVED, + * false otherwise + */ + static native boolean isBackBufferPreserved(); + + static native void nDestroyLayer(long layerPtr); + + private static native void nPrepareTree(long displayListPtr); + + class DrawPerformanceDataProvider extends GraphDataProvider { + private final int mGraphType; + + private int mVerticalUnit; + private int mHorizontalUnit; + private int mHorizontalMargin; + private int mThresholdStroke; + + DrawPerformanceDataProvider(int graphType) { + mGraphType = graphType; + } + + @Override + void prepare(DisplayMetrics metrics) { + final float density = metrics.density; + + mVerticalUnit = dpToPx(PROFILE_DRAW_DP_PER_MS, density); + mHorizontalUnit = dpToPx(PROFILE_DRAW_WIDTH, density); + mHorizontalMargin = dpToPx(PROFILE_DRAW_MARGIN, density); + mThresholdStroke = dpToPx(PROFILE_DRAW_THRESHOLD_STROKE_WIDTH, density); + } + + @Override + int getGraphType() { + return mGraphType; + } + + @Override + int getVerticalUnitSize() { + return mVerticalUnit; + } + + @Override + int getHorizontalUnitSize() { + return mHorizontalUnit; + } + + @Override + int getHorizontaUnitMargin() { + return mHorizontalMargin; + } + + @Override + float[] getData() { + return mProfileData; + } + + @Override + float getThreshold() { + return 16; + } + + @Override + int getFrameCount() { + return mProfileData.length / PROFILE_FRAME_DATA_COUNT; + } + + @Override + int getElementCount() { + return PROFILE_FRAME_DATA_COUNT; + } + + @Override + int getCurrentFrame() { + return mProfileCurrentFrame / PROFILE_FRAME_DATA_COUNT; + } + + @Override + void setupGraphPaint(Paint paint, int elementIndex) { + paint.setColor(PROFILE_DRAW_COLORS[elementIndex]); + if (mGraphType == GRAPH_TYPE_LINES) paint.setStrokeWidth(mThresholdStroke); + } + + @Override + void setupThresholdPaint(Paint paint) { + paint.setColor(PROFILE_DRAW_THRESHOLD_COLOR); + paint.setStrokeWidth(mThresholdStroke); + } + + @Override + void setupCurrentFramePaint(Paint paint) { + paint.setColor(PROFILE_DRAW_CURRENT_FRAME_COLOR); + if (mGraphType == GRAPH_TYPE_LINES) paint.setStrokeWidth(mThresholdStroke); + } + } +} diff --git a/core/java/android/view/HapticFeedbackConstants.java b/core/java/android/view/HapticFeedbackConstants.java index 8f40260..26f47f9 100644 --- a/core/java/android/view/HapticFeedbackConstants.java +++ b/core/java/android/view/HapticFeedbackConstants.java @@ -41,6 +41,11 @@ public class HapticFeedbackConstants { public static final int KEYBOARD_TAP = 3; /** + * The user has pressed either an hour or minute tick of a Clock. + */ + public static final int CLOCK_TICK = 4; + + /** * This is a private constant. Feel free to renumber as desired. * @hide */ diff --git a/core/java/android/view/HardwareCanvas.java b/core/java/android/view/HardwareCanvas.java index 10f700c..233f846 100644 --- a/core/java/android/view/HardwareCanvas.java +++ b/core/java/android/view/HardwareCanvas.java @@ -23,11 +23,10 @@ import android.graphics.Rect; /** * Hardware accelerated canvas. - * + * * @hide */ public abstract class HardwareCanvas extends Canvas { - private String mName; @Override public boolean isHardwareAccelerated() { @@ -40,37 +39,10 @@ public abstract class HardwareCanvas extends Canvas { } /** - * Specifies the name of this canvas. Naming the canvas is entirely - * optional but can be useful for debugging purposes. - * - * @param name The name of the canvas, can be null - * - * @see #getName() - * - * @hide - */ - public void setName(String name) { - mName = name; - } - - /** - * Returns the name of this canvas. - * - * @return The name of the canvas or null - * - * @see #setName(String) - * - * @hide - */ - public String getName() { - return mName; - } - - /** * Invoked before any drawing operation is performed in this canvas. - * + * * @param dirty The dirty rectangle to update, can be null. - * @return {@link DisplayList#STATUS_DREW} if anything was drawn (such as a call to clear + * @return {@link RenderNode#STATUS_DREW} if anything was drawn (such as a call to clear * the canvas). * * @hide @@ -86,40 +58,28 @@ public abstract class HardwareCanvas extends Canvas { /** * Draws the specified display list onto this canvas. The display list can only - * be drawn if {@link android.view.DisplayList#isValid()} returns true. + * be drawn if {@link android.view.RenderNode#isValid()} returns true. * * @param displayList The display list to replay. */ - public void drawDisplayList(DisplayList displayList) { - drawDisplayList(displayList, null, DisplayList.FLAG_CLIP_CHILDREN); + public void drawDisplayList(RenderNode displayList) { + drawDisplayList(displayList, null, RenderNode.FLAG_CLIP_CHILDREN); } /** * Draws the specified display list onto this canvas. * * @param displayList The display list to replay. - * @param dirty The dirty region to redraw in the next pass, matters only - * if this method returns {@link DisplayList#STATUS_DRAW}, can be null. - * @param flags Optional flags about drawing, see {@link DisplayList} for + * @param dirty Ignored, can be null. + * @param flags Optional flags about drawing, see {@link RenderNode} for * the possible flags. * - * @return One of {@link DisplayList#STATUS_DONE}, {@link DisplayList#STATUS_DRAW}, or - * {@link DisplayList#STATUS_INVOKE}, or'd with {@link DisplayList#STATUS_DREW} + * @return One of {@link RenderNode#STATUS_DONE} or {@link RenderNode#STATUS_DREW} * if anything was drawn. * * @hide */ - public abstract int drawDisplayList(DisplayList displayList, Rect dirty, int flags); - - /** - * Outputs the specified display list to the log. This method exists for use by - * tools to output display lists for selected nodes to the log. - * - * @param displayList The display list to be logged. - * - * @hide - */ - abstract void outputDisplayList(DisplayList displayList); + public abstract int drawDisplayList(RenderNode displayList, Rect dirty, int flags); /** * Draws the specified layer onto this canvas. @@ -139,29 +99,27 @@ public abstract class HardwareCanvas extends Canvas { * This function may return true if an invalidation is needed after the call. * * @param drawGLFunction A native function pointer - * - * @return One of {@link DisplayList#STATUS_DONE}, {@link DisplayList#STATUS_DRAW} or - * {@link DisplayList#STATUS_INVOKE} + * + * @return {@link RenderNode#STATUS_DONE} * * @hide */ public int callDrawGLFunction(long drawGLFunction) { // Noop - this is done in the display list recorder subclass - return DisplayList.STATUS_DONE; + return RenderNode.STATUS_DONE; } /** * Invoke all the functors who requested to be invoked during the previous frame. - * - * @param dirty The region to redraw when the functors return {@link DisplayList#STATUS_DRAW} - * - * @return One of {@link DisplayList#STATUS_DONE}, {@link DisplayList#STATUS_DRAW} or - * {@link DisplayList#STATUS_INVOKE} + * + * @param dirty Ignored + * + * @return Ignored * * @hide */ public int invokeFunctors(Rect dirty) { - return DisplayList.STATUS_DONE; + return RenderNode.STATUS_DONE; } /** @@ -192,7 +150,7 @@ public abstract class HardwareCanvas extends Canvas { /** * Indicates that the specified layer must be updated as soon as possible. - * + * * @param layer The layer to update * * @see #clearLayerUpdates() @@ -225,7 +183,7 @@ public abstract class HardwareCanvas extends Canvas { /** * Removes all enqueued layer updates. - * + * * @see #pushLayerUpdate(HardwareLayer) * * @hide diff --git a/core/java/android/view/HardwareLayer.java b/core/java/android/view/HardwareLayer.java index 23383d9..4d78733 100644 --- a/core/java/android/view/HardwareLayer.java +++ b/core/java/android/view/HardwareLayer.java @@ -17,10 +17,10 @@ package android.view; import android.graphics.Bitmap; -import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Rect; +import android.graphics.SurfaceTexture; /** * A hardware layer can be used to render graphics operations into a hardware @@ -28,38 +28,35 @@ import android.graphics.Rect; * would use a Frame Buffer Object (FBO.) The hardware layer can be used as * a drawing cache when a complex set of graphics operations needs to be * drawn several times. + * + * @hide */ -abstract class HardwareLayer { - /** - * Indicates an unknown dimension (width or height.) - */ - static final int DIMENSION_UNDEFINED = -1; - - int mWidth; - int mHeight; - DisplayList mDisplayList; +final class HardwareLayer { + private static final int LAYER_TYPE_TEXTURE = 1; + private static final int LAYER_TYPE_DISPLAY_LIST = 2; - boolean mOpaque; + private HardwareRenderer mRenderer; + private Finalizer mFinalizer; + private RenderNode mDisplayList; + private final int mLayerType; - /** - * Creates a new hardware layer with undefined dimensions. - */ - HardwareLayer() { - this(DIMENSION_UNDEFINED, DIMENSION_UNDEFINED, false); + private HardwareLayer(HardwareRenderer renderer, long deferredUpdater, int type) { + if (renderer == null || deferredUpdater == 0) { + throw new IllegalArgumentException("Either hardware renderer: " + renderer + + " or deferredUpdater: " + deferredUpdater + " is invalid"); + } + mRenderer = renderer; + mLayerType = type; + mFinalizer = new Finalizer(deferredUpdater); + + // Layer is considered initialized at this point, notify the HardwareRenderer + mRenderer.onLayerCreated(this); } - /** - * Creates a new hardware layer at least as large as the supplied - * dimensions. - * - * @param width The minimum width of the layer - * @param height The minimum height of the layer - * @param isOpaque Whether the layer should be opaque or not - */ - HardwareLayer(int width, int height, boolean isOpaque) { - mWidth = width; - mHeight = height; - mOpaque = isOpaque; + private void assertType(int type) { + if (mLayerType != type) { + throw new IllegalAccessError("Method not appropriate for this layer type! " + mLayerType); + } } /** @@ -68,158 +65,244 @@ abstract class HardwareLayer { * @param paint The paint used when the layer is drawn into the destination canvas. * @see View#setLayerPaint(android.graphics.Paint) */ - void setLayerPaint(Paint paint) { } + public void setLayerPaint(Paint paint) { + nSetLayerPaint(mFinalizer.mDeferredUpdater, paint.mNativePaint); + } /** - * Returns the minimum width of the layer. - * - * @return The minimum desired width of the hardware layer + * Indicates whether this layer can be rendered. + * + * @return True if the layer can be rendered into, false otherwise */ - int getWidth() { - return mWidth; + public boolean isValid() { + return mFinalizer != null && mFinalizer.mDeferredUpdater != 0; } /** - * Returns the minimum height of the layer. - * - * @return The minimum desired height of the hardware layer + * Destroys resources without waiting for a GC. */ - int getHeight() { - return mHeight; + public void destroy() { + if (!isValid()) { + // Already destroyed + return; + } + + if (mDisplayList != null) { + mDisplayList.destroyDisplayListData(); + mDisplayList = null; + } + if (mRenderer != null) { + mRenderer.onLayerDestroyed(this); + mRenderer = null; + } + doDestroyLayerUpdater(); } - /** - * Returns the DisplayList for the layer. - * - * @return The DisplayList of the hardware layer - */ - DisplayList getDisplayList() { - return mDisplayList; + public long getDeferredLayerUpdater() { + return mFinalizer.mDeferredUpdater; } /** - * Sets the DisplayList for the layer. + * Destroys the deferred layer updater but not the backing layer. The + * backing layer is instead returned and is the caller's responsibility + * to destroy/recycle as appropriate. * - * @param displayList The new DisplayList for this layer + * It is safe to call this in onLayerDestroyed only */ - void setDisplayList(DisplayList displayList) { - mDisplayList = displayList; + public long detachBackingLayer() { + long backingLayer = nDetachBackingLayer(mFinalizer.mDeferredUpdater); + doDestroyLayerUpdater(); + return backingLayer; } - /** - * Returns whether or not this layer is opaque. - * - * @return True if the layer is opaque, false otherwise - */ - boolean isOpaque() { - return mOpaque; + private void doDestroyLayerUpdater() { + if (mFinalizer != null) { + mFinalizer.destroy(); + mFinalizer = null; + } } - /** - * Sets whether or not this layer should be considered opaque. - * - * @param isOpaque True if the layer is opaque, false otherwise - */ - abstract void setOpaque(boolean isOpaque); + public RenderNode startRecording() { + assertType(LAYER_TYPE_DISPLAY_LIST); - /** - * Indicates whether this layer can be rendered. - * - * @return True if the layer can be rendered into, false otherwise - */ - abstract boolean isValid(); + if (mDisplayList == null) { + mDisplayList = RenderNode.create("HardwareLayer"); + } + return mDisplayList; + } - /** - * Resize the layer, if necessary, to be at least as large - * as the supplied dimensions. - * - * @param width The new desired minimum width for this layer - * @param height The new desired minimum height for this layer - * @return True if the resulting layer is valid, false otherwise - */ - abstract boolean resize(int width, int height); + public void endRecording(Rect dirtyRect) { + nUpdateRenderLayer(mFinalizer.mDeferredUpdater, mDisplayList.getNativeDisplayList(), + dirtyRect.left, dirtyRect.top, dirtyRect.right, dirtyRect.bottom); + mRenderer.pushLayerUpdate(this); + } /** - * Returns a hardware canvas that can be used to render onto - * this layer. - * - * @return A hardware canvas, or null if a canvas cannot be created + * Copies this layer into the specified bitmap. * - * @see #start(android.graphics.Canvas) - * @see #end(android.graphics.Canvas) - */ - abstract HardwareCanvas getCanvas(); - - /** - * Destroys resources without waiting for a GC. + * @param bitmap The bitmap to copy they layer into + * + * @return True if the copy was successful, false otherwise */ - abstract void destroy(); + public boolean copyInto(Bitmap bitmap) { + return mRenderer.copyLayerInto(this, bitmap); + } /** - * This must be invoked before drawing onto this layer. + * Update the layer's properties. Note that after calling this isValid() may + * return false if the requested width/height cannot be satisfied + * + * @param width The new width of this layer + * @param height The new height of this layer + * @param isOpaque Whether this layer is opaque * - * @param currentCanvas The canvas whose rendering needs to be interrupted + * @return true if the layer's properties will change, false if they already + * match the desired values. */ - abstract HardwareCanvas start(Canvas currentCanvas); + public boolean prepare(int width, int height, boolean isOpaque) { + return nPrepare(mFinalizer.mDeferredUpdater, width, height, isOpaque); + } /** - * This must be invoked before drawing onto this layer. + * Sets an optional transform on this layer. * - * @param dirty The dirty area to repaint - * @param currentCanvas The canvas whose rendering needs to be interrupted + * @param matrix The transform to apply to the layer. */ - abstract HardwareCanvas start(Canvas currentCanvas, Rect dirty); + public void setTransform(Matrix matrix) { + nSetTransform(mFinalizer.mDeferredUpdater, matrix.native_instance); + } /** - * This must be invoked after drawing onto this layer. - * - * @param currentCanvas The canvas whose rendering needs to be resumed + * Indicates that this layer has lost its texture. */ - abstract void end(Canvas currentCanvas); + public void detachSurfaceTexture(final SurfaceTexture surface) { + assertType(LAYER_TYPE_TEXTURE); + mRenderer.safelyRun(new Runnable() { + @Override + public void run() { + surface.detachFromGLContext(); + // SurfaceTexture owns the texture name and detachFromGLContext + // should have deleted it + nOnTextureDestroyed(mFinalizer.mDeferredUpdater); + } + }); + } /** - * Copies this layer into the specified bitmap. - * - * @param bitmap The bitmap to copy they layer into - * - * @return True if the copy was successful, false otherwise + * This exists to minimize impact into the current HardwareLayer paths as + * some of the specifics of how to handle error cases in the fully + * deferred model will work */ - abstract boolean copyInto(Bitmap bitmap); + @Deprecated + public void flushChanges() { + if (HardwareRenderer.sUseRenderThread) { + // Not supported, don't try. + return; + } + + boolean success = nFlushChanges(mFinalizer.mDeferredUpdater); + if (!success) { + destroy(); + } + } + + public long getLayer() { + return nGetLayer(mFinalizer.mDeferredUpdater); + } + + public void setSurfaceTexture(SurfaceTexture surface) { + assertType(LAYER_TYPE_TEXTURE); + nSetSurfaceTexture(mFinalizer.mDeferredUpdater, surface, false); + } + + public void updateSurfaceTexture() { + assertType(LAYER_TYPE_TEXTURE); + nUpdateSurfaceTexture(mFinalizer.mDeferredUpdater); + } /** - * Update the layer's properties. This method should be used - * when the underlying storage is modified by an external entity. - * To change the underlying storage, use the {@link #resize(int, int)} - * method instead. - * - * @param width The new width of this layer - * @param height The new height of this layer - * @param isOpaque Whether this layer is opaque + * This should only be used by HardwareRenderer! Do not call directly */ - void update(int width, int height, boolean isOpaque) { - mWidth = width; - mHeight = height; - mOpaque = isOpaque; + SurfaceTexture createSurfaceTexture() { + assertType(LAYER_TYPE_TEXTURE); + SurfaceTexture st = new SurfaceTexture(nGetTexName(mFinalizer.mDeferredUpdater)); + nSetSurfaceTexture(mFinalizer.mDeferredUpdater, st, true); + return st; } /** - * Sets an optional transform on this layer. - * - * @param matrix The transform to apply to the layer. + * This should only be used by HardwareRenderer! Do not call directly */ - abstract void setTransform(Matrix matrix); + static HardwareLayer createTextureLayer(HardwareRenderer renderer) { + return new HardwareLayer(renderer, nCreateTextureLayer(), LAYER_TYPE_TEXTURE); + } + + static HardwareLayer adoptTextureLayer(HardwareRenderer renderer, long layer) { + return new HardwareLayer(renderer, layer, LAYER_TYPE_TEXTURE); + } /** - * Specifies the display list to use to refresh the layer. - * - * @param displayList The display list containing the drawing commands to - * execute in this layer - * @param dirtyRect The dirty region of the layer that needs to be redrawn + * This should only be used by HardwareRenderer! Do not call directly */ - abstract void redrawLater(DisplayList displayList, Rect dirtyRect); + static HardwareLayer createDisplayListLayer(HardwareRenderer renderer, + int width, int height) { + return new HardwareLayer(renderer, nCreateRenderLayer(width, height), LAYER_TYPE_DISPLAY_LIST); + } - /** - * Indicates that this layer has lost its underlying storage. + static HardwareLayer adoptDisplayListLayer(HardwareRenderer renderer, long layer) { + return new HardwareLayer(renderer, layer, LAYER_TYPE_DISPLAY_LIST); + } + + /** This also creates the underlying layer */ + private static native long nCreateTextureLayer(); + private static native long nCreateRenderLayer(int width, int height); + + private static native void nOnTextureDestroyed(long layerUpdater); + private static native long nDetachBackingLayer(long layerUpdater); + + /** This also destroys the underlying layer if it is still attached. + * Note it does not recycle the underlying layer, but instead queues it + * for deferred deletion. + * The HardwareRenderer should use detachBackingLayer() in the + * onLayerDestroyed() callback to do recycling if desired. */ - abstract void clearStorage(); + private static native void nDestroyLayerUpdater(long layerUpdater); + + private static native boolean nPrepare(long layerUpdater, int width, int height, boolean isOpaque); + private static native void nSetLayerPaint(long layerUpdater, long paint); + private static native void nSetTransform(long layerUpdater, long matrix); + private static native void nSetSurfaceTexture(long layerUpdater, + SurfaceTexture surface, boolean isAlreadyAttached); + private static native void nUpdateSurfaceTexture(long layerUpdater); + private static native void nUpdateRenderLayer(long layerUpdater, long displayList, + int left, int top, int right, int bottom); + + private static native boolean nFlushChanges(long layerUpdater); + + private static native long nGetLayer(long layerUpdater); + private static native int nGetTexName(long layerUpdater); + + private static class Finalizer { + private long mDeferredUpdater; + + public Finalizer(long deferredUpdater) { + mDeferredUpdater = deferredUpdater; + } + + @Override + protected void finalize() throws Throwable { + try { + destroy(); + } finally { + super.finalize(); + } + } + + void destroy() { + if (mDeferredUpdater != 0) { + nDestroyLayerUpdater(mDeferredUpdater); + mDeferredUpdater = 0; + } + } + } } diff --git a/core/java/android/view/HardwareRenderer.java b/core/java/android/view/HardwareRenderer.java index f09a111..56d96e1 100644 --- a/core/java/android/view/HardwareRenderer.java +++ b/core/java/android/view/HardwareRenderer.java @@ -16,41 +16,15 @@ package android.view; -import android.content.ComponentCallbacks2; -import android.graphics.Color; +import android.graphics.Bitmap; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.SurfaceTexture; -import android.opengl.EGL14; -import android.opengl.GLUtils; -import android.opengl.ManagedEGLContext; -import android.os.Handler; -import android.os.IBinder; -import android.os.Looper; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.os.SystemClock; -import android.os.SystemProperties; -import android.os.Trace; import android.util.DisplayMetrics; -import android.util.Log; import android.view.Surface.OutOfResourcesException; -import com.google.android.gles_jni.EGLImpl; - -import javax.microedition.khronos.egl.EGL10; -import javax.microedition.khronos.egl.EGL11; -import javax.microedition.khronos.egl.EGLConfig; -import javax.microedition.khronos.egl.EGLContext; -import javax.microedition.khronos.egl.EGLDisplay; -import javax.microedition.khronos.egl.EGLSurface; -import javax.microedition.khronos.opengles.GL; - import java.io.File; import java.io.PrintWriter; -import java.util.concurrent.locks.ReentrantLock; - -import static javax.microedition.khronos.egl.EGL10.*; /** * Interface for rendering a view hierarchy using hardware acceleration. @@ -66,13 +40,6 @@ public abstract class HardwareRenderer { private static final String CACHE_PATH_SHADERS = "com.android.opengl.shaders_cache"; /** - * Turn on to only refresh the parts of the screen that need updating. - * When turned on the property defined by {@link #RENDER_DIRTY_REGIONS_PROPERTY} - * must also have the value "true". - */ - static final boolean RENDER_DIRTY_REGIONS = true; - - /** * System property used to enable or disable dirty regions invalidation. * This property is only queried if {@link #RENDER_DIRTY_REGIONS} is true. * The default value of this property is assumed to be true. @@ -187,14 +154,6 @@ public abstract class HardwareRenderer { public static final String OVERDRAW_PROPERTY_SHOW = "show"; /** - * Value for {@link #DEBUG_OVERDRAW_PROPERTY}. When the property is set to this - * value, an overdraw counter will be shown on screen. - * - * @hide - */ - public static final String OVERDRAW_PROPERTY_COUNT = "count"; - - /** * Turn on to debug non-rectangular clip operations. * * Possible values: @@ -222,15 +181,8 @@ public abstract class HardwareRenderer { */ public static boolean sSystemRendererDisabled = false; - /** - * Number of frames to profile. - */ - private static final int PROFILE_MAX_FRAMES = 128; - - /** - * Number of floats per profiled frame. - */ - private static final int PROFILE_FRAME_DATA_COUNT = 3; + /** @hide */ + public static boolean sUseRenderThread = true; private boolean mEnabled; private boolean mRequested = true; @@ -282,11 +234,11 @@ public abstract class HardwareRenderer { abstract void updateSurface(Surface surface) throws OutOfResourcesException; /** - * Destroys the layers used by the specified view hierarchy. - * - * @param view The root of the view hierarchy + * Stops any rendering into the surface. Use this if it is unclear whether + * or not the surface used by the HardwareRenderer will be changing. It + * Suspends any rendering into the surface, but will not do any destruction */ - abstract void destroyLayers(View view); + abstract void pauseSurface(Surface surface); /** * Destroys all hardware rendering resources associated with the specified @@ -305,15 +257,6 @@ public abstract class HardwareRenderer { abstract void invalidate(Surface surface); /** - * This method should be invoked to ensure the hardware renderer is in - * valid state (for instance, to ensure the correct EGL context is bound - * to the current thread.) - * - * @return true if the renderer is now valid, false otherwise - */ - abstract boolean validate(); - - /** * This method ensures the hardware renderer is in a valid state * before executing the specified action. * @@ -350,13 +293,6 @@ public abstract class HardwareRenderer { abstract int getHeight(); /** - * Gets the current canvas associated with this HardwareRenderer. - * - * @return the current HardwareCanvas - */ - abstract HardwareCanvas getCanvas(); - - /** * Outputs extra debugging information in the specified file descriptor. * @param pw */ @@ -379,9 +315,7 @@ public abstract class HardwareRenderer { * * @return True if a property has changed. */ - abstract boolean loadSystemProperties(Surface surface); - - private static native boolean nLoadProperties(); + abstract boolean loadSystemProperties(); /** * Sets the directory to use as a persistent storage for hardware rendering @@ -392,60 +326,9 @@ public abstract class HardwareRenderer { * @hide */ public static void setupDiskCache(File cacheDir) { - nSetupShadersDiskCache(new File(cacheDir, CACHE_PATH_SHADERS).getAbsolutePath()); + GLRenderer.setupShadersDiskCache(new File(cacheDir, CACHE_PATH_SHADERS).getAbsolutePath()); } - private static native void nSetupShadersDiskCache(String cacheFile); - - /** - * Notifies EGL that the frame is about to be rendered. - * @param size - */ - static void beginFrame(int[] size) { - nBeginFrame(size); - } - - private static native void nBeginFrame(int[] size); - - /** - * Returns the current system time according to the renderer. - * This method is used for debugging only and should not be used - * as a clock. - */ - static long getSystemTime() { - return nGetSystemTime(); - } - - private static native long nGetSystemTime(); - - /** - * Preserves the back buffer of the current surface after a buffer swap. - * Calling this method sets the EGL_SWAP_BEHAVIOR attribute of the current - * surface to EGL_BUFFER_PRESERVED. Calling this method requires an EGL - * config that supports EGL_SWAP_BEHAVIOR_PRESERVED_BIT. - * - * @return True if the swap behavior was successfully changed, - * false otherwise. - */ - static boolean preserveBackBuffer() { - return nPreserveBackBuffer(); - } - - private static native boolean nPreserveBackBuffer(); - - /** - * Indicates whether the current surface preserves its back buffer - * after a buffer swap. - * - * @return True, if the surface's EGL_SWAP_BEHAVIOR is EGL_BUFFER_PRESERVED, - * false otherwise - */ - static boolean isBackBufferPreserved() { - return nIsBackBufferPreserved(); - } - - private static native boolean nIsBackBufferPreserved(); - /** * Indicates that the specified hardware layer needs to be updated * as soon as possible. @@ -453,19 +336,20 @@ public abstract class HardwareRenderer { * @param layer The hardware layer that needs an update * * @see #flushLayerUpdates() - * @see #cancelLayerUpdate(HardwareLayer) */ abstract void pushLayerUpdate(HardwareLayer layer); /** - * Cancels a queued layer update. If the specified layer was not - * queued for update, this method has no effect. - * - * @param layer The layer whose update to cancel - * - * @see #pushLayerUpdate(HardwareLayer) + * Tells the HardwareRenderer that a layer was created. The renderer should + * make sure to apply any pending layer changes at the start of a new frame */ - abstract void cancelLayerUpdate(HardwareLayer layer); + abstract void onLayerCreated(HardwareLayer hardwareLayer); + + /** + * Tells the HardwareRenderer that the layer is destroyed. The renderer + * should remove the layer from any update queues. + */ + abstract void onLayerDestroyed(HardwareLayer layer); /** * Forces all enqueued layer updates to be executed immediately. @@ -509,37 +393,22 @@ public abstract class HardwareRenderer { Rect dirty); /** - * Creates a new display list that can be used to record batches of - * drawing operations. - * - * @param name The name of the display list, used for debugging purpose. May be null. - * - * @return A new display list. - * - * @hide - */ - public abstract DisplayList createDisplayList(String name); - - /** * Creates a new hardware layer. A hardware layer built by calling this * method will be treated as a texture layer, instead of as a render target. * - * @param isOpaque Whether the layer should be opaque or not - * * @return A hardware layer */ - abstract HardwareLayer createHardwareLayer(boolean isOpaque); + abstract HardwareLayer createTextureLayer(); /** * Creates a new hardware layer. * * @param width The minimum width of the layer * @param height The minimum height of the layer - * @param isOpaque Whether the layer should be opaque or not * * @return A hardware layer */ - abstract HardwareLayer createHardwareLayer(int width, int height, boolean isOpaque); + abstract HardwareLayer createDisplayListLayer(int width, int height); /** * Creates a new {@link SurfaceTexture} that can be used to render into the @@ -551,14 +420,7 @@ public abstract class HardwareRenderer { */ abstract SurfaceTexture createSurfaceTexture(HardwareLayer layer); - /** - * Sets the {@link android.graphics.SurfaceTexture} that will be used to - * render into the specified hardware layer. - * - * @param layer The layer to render into using a {@link android.graphics.SurfaceTexture} - * @param surfaceTexture The {@link android.graphics.SurfaceTexture} to use for the layer - */ - abstract void setSurfaceTexture(HardwareLayer layer, SurfaceTexture surfaceTexture); + abstract boolean copyLayerInto(HardwareLayer layer, Bitmap bitmap); /** * Detaches the specified functor from the current functor execution queue. @@ -566,7 +428,7 @@ public abstract class HardwareRenderer { * @param functor The native functor to remove from the execution queue. * * @see HardwareCanvas#callDrawGLFunction(int) - * @see #attachFunctor(android.view.View.AttachInfo, int) + * @see #attachFunctor(android.view.View.AttachInfo, long) */ abstract void detachFunctor(long functor); @@ -577,11 +439,21 @@ public abstract class HardwareRenderer { * @param functor The native functor to insert in the execution queue. * * @see HardwareCanvas#callDrawGLFunction(int) - * @see #detachFunctor(int) + * @see #detachFunctor(long) + * + */ + abstract void attachFunctor(View.AttachInfo attachInfo, long functor); + + /** + * Schedules the functor for execution in either kModeProcess or + * kModeProcessNoContext, depending on whether or not there is an EGLContext. * - * @return true if the functor was attached successfully + * @param functor The native functor to invoke + * @param waitForCompletion If true, this will not return until the functor + * has invoked. If false, the functor may be invoked + * asynchronously. */ - abstract boolean attachFunctor(View.AttachInfo attachInfo, long functor); + public abstract void invokeFunctor(long functor, boolean waitForCompletion); /** * Initializes the hardware renderer for the specified surface and setup the @@ -621,17 +493,20 @@ public abstract class HardwareRenderer { /** * Creates a hardware renderer using OpenGL. * - * @param glVersion The version of OpenGL to use (1 for OpenGL 1, 11 for OpenGL 1.1, etc.) * @param translucent True if the surface is translucent, false otherwise * * @return A hardware renderer backed by OpenGL. */ - static HardwareRenderer createGlRenderer(int glVersion, boolean translucent) { - switch (glVersion) { - case 2: - return Gl20Renderer.create(translucent); + static HardwareRenderer create(boolean translucent) { + HardwareRenderer renderer = null; + if (GLES20Canvas.isAvailable()) { + if (sUseRenderThread) { + renderer = new ThreadedRenderer(translucent); + } else { + renderer = new GLRenderer(translucent); + } } - throw new IllegalArgumentException("Unknown GL version: " + glVersion); + return renderer; } /** @@ -656,7 +531,7 @@ public abstract class HardwareRenderer { * see {@link android.content.ComponentCallbacks} */ static void startTrimMemory(int level) { - Gl20Renderer.startTrimMemory(level); + GLRenderer.startTrimMemory(level); } /** @@ -664,7 +539,7 @@ public abstract class HardwareRenderer { * cleanup special resources used by the memory trimming process. */ static void endTrimMemory() { - Gl20Renderer.endTrimMemory(); + GLRenderer.endTrimMemory(); } /** @@ -706,6 +581,11 @@ public abstract class HardwareRenderer { } /** + * Blocks until all previously queued work has completed. + */ + abstract void fence(); + + /** * Describes a series of frames that should be drawn on screen as a graph. * Each frame is composed of 1 or more elements. */ @@ -798,1553 +678,4 @@ public abstract class HardwareRenderer { */ abstract void setupCurrentFramePaint(Paint paint); } - - @SuppressWarnings({"deprecation"}) - static abstract class GlRenderer extends HardwareRenderer { - static final int SURFACE_STATE_ERROR = 0; - static final int SURFACE_STATE_SUCCESS = 1; - static final int SURFACE_STATE_UPDATED = 2; - - static final int FUNCTOR_PROCESS_DELAY = 4; - - private static final int PROFILE_DRAW_MARGIN = 0; - private static final int PROFILE_DRAW_WIDTH = 3; - private static final int[] PROFILE_DRAW_COLORS = { 0xcf3e66cc, 0xcfdc3912, 0xcfe69800 }; - private static final int PROFILE_DRAW_CURRENT_FRAME_COLOR = 0xcf5faa4d; - private static final int PROFILE_DRAW_THRESHOLD_COLOR = 0xff5faa4d; - private static final int PROFILE_DRAW_THRESHOLD_STROKE_WIDTH = 2; - private static final int PROFILE_DRAW_DP_PER_MS = 7; - - private static final String[] VISUALIZERS = { - PROFILE_PROPERTY_VISUALIZE_BARS, - PROFILE_PROPERTY_VISUALIZE_LINES - }; - - private static final String[] OVERDRAW = { - OVERDRAW_PROPERTY_SHOW, - OVERDRAW_PROPERTY_COUNT - }; - private static final int OVERDRAW_TYPE_COUNT = 1; - - static EGL10 sEgl; - static EGLDisplay sEglDisplay; - static EGLConfig sEglConfig; - static final Object[] sEglLock = new Object[0]; - int mWidth = -1, mHeight = -1; - - static final ThreadLocal<ManagedEGLContext> sEglContextStorage - = new ThreadLocal<ManagedEGLContext>(); - - EGLContext mEglContext; - Thread mEglThread; - - EGLSurface mEglSurface; - - GL mGl; - HardwareCanvas mCanvas; - - String mName; - - long mFrameCount; - Paint mDebugPaint; - - static boolean sDirtyRegions; - static final boolean sDirtyRegionsRequested; - static { - String dirtyProperty = SystemProperties.get(RENDER_DIRTY_REGIONS_PROPERTY, "true"); - //noinspection PointlessBooleanExpression,ConstantConditions - sDirtyRegions = RENDER_DIRTY_REGIONS && "true".equalsIgnoreCase(dirtyProperty); - sDirtyRegionsRequested = sDirtyRegions; - } - - boolean mDirtyRegionsEnabled; - boolean mUpdateDirtyRegions; - - boolean mProfileEnabled; - int mProfileVisualizerType = -1; - float[] mProfileData; - ReentrantLock mProfileLock; - int mProfileCurrentFrame = -PROFILE_FRAME_DATA_COUNT; - - GraphDataProvider mDebugDataProvider; - float[][] mProfileShapes; - Paint mProfilePaint; - - boolean mDebugDirtyRegions; - int mDebugOverdraw = -1; - HardwareLayer mDebugOverdrawLayer; - Paint mDebugOverdrawPaint; - - final int mGlVersion; - final boolean mTranslucent; - - private boolean mDestroyed; - - private final Rect mRedrawClip = new Rect(); - - private final int[] mSurfaceSize = new int[2]; - private final FunctorsRunnable mFunctorsRunnable = new FunctorsRunnable(); - - private long mDrawDelta = Long.MAX_VALUE; - - GlRenderer(int glVersion, boolean translucent) { - mGlVersion = glVersion; - mTranslucent = translucent; - - loadSystemProperties(null); - } - - @Override - boolean loadSystemProperties(Surface surface) { - boolean value; - boolean changed = false; - - String profiling = SystemProperties.get(PROFILE_PROPERTY); - int graphType = search(VISUALIZERS, profiling); - value = graphType >= 0; - - if (graphType != mProfileVisualizerType) { - changed = true; - mProfileVisualizerType = graphType; - - mProfileShapes = null; - mProfilePaint = null; - - if (value) { - mDebugDataProvider = new DrawPerformanceDataProvider(graphType); - } else { - mDebugDataProvider = null; - } - } - - // If on-screen profiling is not enabled, we need to check whether - // console profiling only is enabled - if (!value) { - value = Boolean.parseBoolean(profiling); - } - - if (value != mProfileEnabled) { - changed = true; - mProfileEnabled = value; - - if (mProfileEnabled) { - Log.d(LOG_TAG, "Profiling hardware renderer"); - - int maxProfileFrames = SystemProperties.getInt(PROFILE_MAXFRAMES_PROPERTY, - PROFILE_MAX_FRAMES); - mProfileData = new float[maxProfileFrames * PROFILE_FRAME_DATA_COUNT]; - for (int i = 0; i < mProfileData.length; i += PROFILE_FRAME_DATA_COUNT) { - mProfileData[i] = mProfileData[i + 1] = mProfileData[i + 2] = -1; - } - - mProfileLock = new ReentrantLock(); - } else { - mProfileData = null; - mProfileLock = null; - mProfileVisualizerType = -1; - } - - mProfileCurrentFrame = -PROFILE_FRAME_DATA_COUNT; - } - - value = SystemProperties.getBoolean(DEBUG_DIRTY_REGIONS_PROPERTY, false); - if (value != mDebugDirtyRegions) { - changed = true; - mDebugDirtyRegions = value; - - if (mDebugDirtyRegions) { - Log.d(LOG_TAG, "Debugging dirty regions"); - } - } - - String overdraw = SystemProperties.get(HardwareRenderer.DEBUG_OVERDRAW_PROPERTY); - int debugOverdraw = search(OVERDRAW, overdraw); - if (debugOverdraw != mDebugOverdraw) { - changed = true; - mDebugOverdraw = debugOverdraw; - - if (mDebugOverdraw != OVERDRAW_TYPE_COUNT) { - if (mDebugOverdrawLayer != null) { - mDebugOverdrawLayer.destroy(); - mDebugOverdrawLayer = null; - mDebugOverdrawPaint = null; - } - } - } - - if (nLoadProperties()) { - changed = true; - } - - return changed; - } - - private static int search(String[] values, String value) { - for (int i = 0; i < values.length; i++) { - if (values[i].equals(value)) return i; - } - return -1; - } - - @Override - void dumpGfxInfo(PrintWriter pw) { - if (mProfileEnabled) { - pw.printf("\n\tDraw\tProcess\tExecute\n"); - - mProfileLock.lock(); - try { - for (int i = 0; i < mProfileData.length; i += PROFILE_FRAME_DATA_COUNT) { - if (mProfileData[i] < 0) { - break; - } - pw.printf("\t%3.2f\t%3.2f\t%3.2f\n", mProfileData[i], mProfileData[i + 1], - mProfileData[i + 2]); - mProfileData[i] = mProfileData[i + 1] = mProfileData[i + 2] = -1; - } - mProfileCurrentFrame = mProfileData.length; - } finally { - mProfileLock.unlock(); - } - } - } - - @Override - long getFrameCount() { - return mFrameCount; - } - - /** - * Indicates whether this renderer instance can track and update dirty regions. - */ - boolean hasDirtyRegions() { - return mDirtyRegionsEnabled; - } - - /** - * Checks for OpenGL errors. If an error has occured, {@link #destroy(boolean)} - * is invoked and the requested flag is turned off. The error code is - * also logged as a warning. - */ - void checkEglErrors() { - if (isEnabled()) { - checkEglErrorsForced(); - } - } - - private void checkEglErrorsForced() { - int error = sEgl.eglGetError(); - if (error != EGL_SUCCESS) { - // something bad has happened revert to - // normal rendering. - Log.w(LOG_TAG, "EGL error: " + GLUtils.getEGLErrorString(error)); - fallback(error != EGL11.EGL_CONTEXT_LOST); - } - } - - private void fallback(boolean fallback) { - destroy(true); - if (fallback) { - // we'll try again if it was context lost - setRequested(false); - Log.w(LOG_TAG, "Mountain View, we've had a problem here. " - + "Switching back to software rendering."); - } - } - - @Override - boolean initialize(Surface surface) throws OutOfResourcesException { - if (isRequested() && !isEnabled()) { - boolean contextCreated = initializeEgl(); - mGl = createEglSurface(surface); - mDestroyed = false; - - if (mGl != null) { - int err = sEgl.eglGetError(); - if (err != EGL_SUCCESS) { - destroy(true); - setRequested(false); - } else { - if (mCanvas == null) { - mCanvas = createCanvas(); - mCanvas.setName(mName); - } - setEnabled(true); - - if (contextCreated) { - initAtlas(); - } - } - - return mCanvas != null; - } - } - return false; - } - - @Override - void updateSurface(Surface surface) throws OutOfResourcesException { - if (isRequested() && isEnabled()) { - createEglSurface(surface); - } - } - - abstract HardwareCanvas createCanvas(); - - abstract int[] getConfig(boolean dirtyRegions); - - boolean initializeEgl() { - synchronized (sEglLock) { - if (sEgl == null && sEglConfig == null) { - sEgl = (EGL10) EGLContext.getEGL(); - - // Get to the default display. - sEglDisplay = sEgl.eglGetDisplay(EGL_DEFAULT_DISPLAY); - - if (sEglDisplay == EGL_NO_DISPLAY) { - throw new RuntimeException("eglGetDisplay failed " - + GLUtils.getEGLErrorString(sEgl.eglGetError())); - } - - // We can now initialize EGL for that display - int[] version = new int[2]; - if (!sEgl.eglInitialize(sEglDisplay, version)) { - throw new RuntimeException("eglInitialize failed " + - GLUtils.getEGLErrorString(sEgl.eglGetError())); - } - - checkEglErrorsForced(); - - sEglConfig = loadEglConfig(); - } - } - - ManagedEGLContext managedContext = sEglContextStorage.get(); - mEglContext = managedContext != null ? managedContext.getContext() : null; - mEglThread = Thread.currentThread(); - - if (mEglContext == null) { - mEglContext = createContext(sEgl, sEglDisplay, sEglConfig); - sEglContextStorage.set(createManagedContext(mEglContext)); - return true; - } - - return false; - } - - private EGLConfig loadEglConfig() { - EGLConfig eglConfig = chooseEglConfig(); - if (eglConfig == null) { - // We tried to use EGL_SWAP_BEHAVIOR_PRESERVED_BIT, try again without - if (sDirtyRegions) { - sDirtyRegions = false; - eglConfig = chooseEglConfig(); - if (eglConfig == null) { - throw new RuntimeException("eglConfig not initialized"); - } - } else { - throw new RuntimeException("eglConfig not initialized"); - } - } - return eglConfig; - } - - abstract ManagedEGLContext createManagedContext(EGLContext eglContext); - - private EGLConfig chooseEglConfig() { - EGLConfig[] configs = new EGLConfig[1]; - int[] configsCount = new int[1]; - int[] configSpec = getConfig(sDirtyRegions); - - // Debug - final String debug = SystemProperties.get(PRINT_CONFIG_PROPERTY, ""); - if ("all".equalsIgnoreCase(debug)) { - sEgl.eglChooseConfig(sEglDisplay, configSpec, null, 0, configsCount); - - EGLConfig[] debugConfigs = new EGLConfig[configsCount[0]]; - sEgl.eglChooseConfig(sEglDisplay, configSpec, debugConfigs, - configsCount[0], configsCount); - - for (EGLConfig config : debugConfigs) { - printConfig(config); - } - } - - if (!sEgl.eglChooseConfig(sEglDisplay, configSpec, configs, 1, configsCount)) { - throw new IllegalArgumentException("eglChooseConfig failed " + - GLUtils.getEGLErrorString(sEgl.eglGetError())); - } else if (configsCount[0] > 0) { - if ("choice".equalsIgnoreCase(debug)) { - printConfig(configs[0]); - } - return configs[0]; - } - - return null; - } - - private static void printConfig(EGLConfig config) { - int[] value = new int[1]; - - Log.d(LOG_TAG, "EGL configuration " + config + ":"); - - sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_RED_SIZE, value); - Log.d(LOG_TAG, " RED_SIZE = " + value[0]); - - sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_GREEN_SIZE, value); - Log.d(LOG_TAG, " GREEN_SIZE = " + value[0]); - - sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_BLUE_SIZE, value); - Log.d(LOG_TAG, " BLUE_SIZE = " + value[0]); - - sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_ALPHA_SIZE, value); - Log.d(LOG_TAG, " ALPHA_SIZE = " + value[0]); - - sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_DEPTH_SIZE, value); - Log.d(LOG_TAG, " DEPTH_SIZE = " + value[0]); - - sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_STENCIL_SIZE, value); - Log.d(LOG_TAG, " STENCIL_SIZE = " + value[0]); - - sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_SAMPLE_BUFFERS, value); - Log.d(LOG_TAG, " SAMPLE_BUFFERS = " + value[0]); - - sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_SAMPLES, value); - Log.d(LOG_TAG, " SAMPLES = " + value[0]); - - sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_SURFACE_TYPE, value); - Log.d(LOG_TAG, " SURFACE_TYPE = 0x" + Integer.toHexString(value[0])); - - sEgl.eglGetConfigAttrib(sEglDisplay, config, EGL_CONFIG_CAVEAT, value); - Log.d(LOG_TAG, " CONFIG_CAVEAT = 0x" + Integer.toHexString(value[0])); - } - - GL createEglSurface(Surface surface) throws OutOfResourcesException { - // Check preconditions. - if (sEgl == null) { - throw new RuntimeException("egl not initialized"); - } - if (sEglDisplay == null) { - throw new RuntimeException("eglDisplay not initialized"); - } - if (sEglConfig == null) { - throw new RuntimeException("eglConfig not initialized"); - } - if (Thread.currentThread() != mEglThread) { - throw new IllegalStateException("HardwareRenderer cannot be used " - + "from multiple threads"); - } - - // In case we need to destroy an existing surface - destroySurface(); - - // Create an EGL surface we can render into. - if (!createSurface(surface)) { - return null; - } - - initCaches(); - - return mEglContext.getGL(); - } - - private void enableDirtyRegions() { - // If mDirtyRegions is set, this means we have an EGL configuration - // with EGL_SWAP_BEHAVIOR_PRESERVED_BIT set - if (sDirtyRegions) { - if (!(mDirtyRegionsEnabled = preserveBackBuffer())) { - Log.w(LOG_TAG, "Backbuffer cannot be preserved"); - } - } else if (sDirtyRegionsRequested) { - // If mDirtyRegions is not set, our EGL configuration does not - // have EGL_SWAP_BEHAVIOR_PRESERVED_BIT; however, the default - // swap behavior might be EGL_BUFFER_PRESERVED, which means we - // want to set mDirtyRegions. We try to do this only if dirty - // regions were initially requested as part of the device - // configuration (see RENDER_DIRTY_REGIONS) - mDirtyRegionsEnabled = isBackBufferPreserved(); - } - } - - abstract void initCaches(); - abstract void initAtlas(); - - EGLContext createContext(EGL10 egl, EGLDisplay eglDisplay, EGLConfig eglConfig) { - int[] attribs = { EGL14.EGL_CONTEXT_CLIENT_VERSION, mGlVersion, EGL_NONE }; - - EGLContext context = egl.eglCreateContext(eglDisplay, eglConfig, EGL_NO_CONTEXT, - mGlVersion != 0 ? attribs : null); - if (context == null || context == EGL_NO_CONTEXT) { - //noinspection ConstantConditions - throw new IllegalStateException( - "Could not create an EGL context. eglCreateContext failed with error: " + - GLUtils.getEGLErrorString(sEgl.eglGetError())); - } - - return context; - } - - @Override - void destroy(boolean full) { - if (full && mCanvas != null) { - mCanvas = null; - } - - if (!isEnabled() || mDestroyed) { - setEnabled(false); - return; - } - - destroySurface(); - setEnabled(false); - - mDestroyed = true; - mGl = null; - } - - void destroySurface() { - if (mEglSurface != null && mEglSurface != EGL_NO_SURFACE) { - if (mEglSurface.equals(sEgl.eglGetCurrentSurface(EGL_DRAW))) { - sEgl.eglMakeCurrent(sEglDisplay, - EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); - } - sEgl.eglDestroySurface(sEglDisplay, mEglSurface); - mEglSurface = null; - } - } - - @Override - void invalidate(Surface surface) { - // Cancels any existing buffer to ensure we'll get a buffer - // of the right size before we call eglSwapBuffers - sEgl.eglMakeCurrent(sEglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); - - if (mEglSurface != null && mEglSurface != EGL_NO_SURFACE) { - sEgl.eglDestroySurface(sEglDisplay, mEglSurface); - mEglSurface = null; - setEnabled(false); - } - - if (surface.isValid()) { - if (!createSurface(surface)) { - return; - } - - mUpdateDirtyRegions = true; - - if (mCanvas != null) { - setEnabled(true); - } - } - } - - private boolean createSurface(Surface surface) { - mEglSurface = sEgl.eglCreateWindowSurface(sEglDisplay, sEglConfig, surface, null); - - if (mEglSurface == null || mEglSurface == EGL_NO_SURFACE) { - int error = sEgl.eglGetError(); - if (error == EGL_BAD_NATIVE_WINDOW) { - Log.e(LOG_TAG, "createWindowSurface returned EGL_BAD_NATIVE_WINDOW."); - return false; - } - throw new RuntimeException("createWindowSurface failed " - + GLUtils.getEGLErrorString(error)); - } - - if (!sEgl.eglMakeCurrent(sEglDisplay, mEglSurface, mEglSurface, mEglContext)) { - throw new IllegalStateException("eglMakeCurrent failed " + - GLUtils.getEGLErrorString(sEgl.eglGetError())); - } - - enableDirtyRegions(); - - return true; - } - - @Override - boolean validate() { - return checkRenderContext() != SURFACE_STATE_ERROR; - } - - @Override - void setup(int width, int height) { - if (validate()) { - mCanvas.setViewport(width, height); - mWidth = width; - mHeight = height; - } - } - - @Override - int getWidth() { - return mWidth; - } - - @Override - int getHeight() { - return mHeight; - } - - @Override - HardwareCanvas getCanvas() { - return mCanvas; - } - - @Override - void setName(String name) { - mName = name; - } - - boolean canDraw() { - return mGl != null && mCanvas != null; - } - - int onPreDraw(Rect dirty) { - return DisplayList.STATUS_DONE; - } - - void onPostDraw() { - } - - class FunctorsRunnable implements Runnable { - View.AttachInfo attachInfo; - - @Override - public void run() { - final HardwareRenderer renderer = attachInfo.mHardwareRenderer; - if (renderer == null || !renderer.isEnabled() || renderer != GlRenderer.this) { - return; - } - - if (checkRenderContext() != SURFACE_STATE_ERROR) { - int status = mCanvas.invokeFunctors(mRedrawClip); - handleFunctorStatus(attachInfo, status); - } - } - } - - @Override - void draw(View view, View.AttachInfo attachInfo, HardwareDrawCallbacks callbacks, - Rect dirty) { - if (canDraw()) { - if (!hasDirtyRegions()) { - dirty = null; - } - attachInfo.mIgnoreDirtyState = true; - attachInfo.mDrawingTime = SystemClock.uptimeMillis(); - - view.mPrivateFlags |= View.PFLAG_DRAWN; - - // We are already on the correct thread - final int surfaceState = checkRenderContextUnsafe(); - if (surfaceState != SURFACE_STATE_ERROR) { - HardwareCanvas canvas = mCanvas; - attachInfo.mHardwareCanvas = canvas; - - if (mProfileEnabled) { - mProfileLock.lock(); - } - - dirty = beginFrame(canvas, dirty, surfaceState); - - DisplayList displayList = buildDisplayList(view, canvas); - - // buildDisplayList() calls into user code which can cause - // an eglMakeCurrent to happen with a different surface/context. - // We must therefore check again here. - if (checkRenderContextUnsafe() == SURFACE_STATE_ERROR) { - return; - } - - int saveCount = 0; - int status = DisplayList.STATUS_DONE; - - long start = getSystemTime(); - try { - status = prepareFrame(dirty); - - saveCount = canvas.save(); - callbacks.onHardwarePreDraw(canvas); - - if (displayList != null) { - status |= drawDisplayList(attachInfo, canvas, displayList, status); - } else { - // Shouldn't reach here - view.draw(canvas); - } - } catch (Exception e) { - Log.e(LOG_TAG, "An error has occurred while drawing:", e); - } finally { - callbacks.onHardwarePostDraw(canvas); - canvas.restoreToCount(saveCount); - view.mRecreateDisplayList = false; - - mDrawDelta = getSystemTime() - start; - - if (mDrawDelta > 0) { - mFrameCount++; - - debugOverdraw(attachInfo, dirty, canvas, displayList); - debugDirtyRegions(dirty, canvas); - drawProfileData(attachInfo); - } - } - - onPostDraw(); - - swapBuffers(status); - - if (mProfileEnabled) { - mProfileLock.unlock(); - } - - attachInfo.mIgnoreDirtyState = false; - } - } - } - - abstract void countOverdraw(HardwareCanvas canvas); - abstract float getOverdraw(HardwareCanvas canvas); - - private void debugOverdraw(View.AttachInfo attachInfo, Rect dirty, - HardwareCanvas canvas, DisplayList displayList) { - - if (mDebugOverdraw == OVERDRAW_TYPE_COUNT) { - if (mDebugOverdrawLayer == null) { - mDebugOverdrawLayer = createHardwareLayer(mWidth, mHeight, true); - } else if (mDebugOverdrawLayer.getWidth() != mWidth || - mDebugOverdrawLayer.getHeight() != mHeight) { - mDebugOverdrawLayer.resize(mWidth, mHeight); - } - - if (!mDebugOverdrawLayer.isValid()) { - mDebugOverdraw = -1; - return; - } - - HardwareCanvas layerCanvas = mDebugOverdrawLayer.start(canvas, dirty); - countOverdraw(layerCanvas); - final int restoreCount = layerCanvas.save(); - layerCanvas.drawDisplayList(displayList, null, DisplayList.FLAG_CLIP_CHILDREN); - layerCanvas.restoreToCount(restoreCount); - mDebugOverdrawLayer.end(canvas); - - float overdraw = getOverdraw(layerCanvas); - DisplayMetrics metrics = attachInfo.mRootView.getResources().getDisplayMetrics(); - - drawOverdrawCounter(canvas, overdraw, metrics.density); - } - } - - private void drawOverdrawCounter(HardwareCanvas canvas, float overdraw, float density) { - final String text = String.format("%.2fx", overdraw); - final Paint paint = setupPaint(density); - // HSBtoColor will clamp the values in the 0..1 range - paint.setColor(Color.HSBtoColor(0.28f - 0.28f * overdraw / 3.5f, 0.8f, 1.0f)); - - canvas.drawText(text, density * 4.0f, mHeight - paint.getFontMetrics().bottom, paint); - } - - private Paint setupPaint(float density) { - if (mDebugOverdrawPaint == null) { - mDebugOverdrawPaint = new Paint(); - mDebugOverdrawPaint.setAntiAlias(true); - mDebugOverdrawPaint.setShadowLayer(density * 3.0f, 0.0f, 0.0f, 0xff000000); - mDebugOverdrawPaint.setTextSize(density * 20.0f); - } - return mDebugOverdrawPaint; - } - - private DisplayList buildDisplayList(View view, HardwareCanvas canvas) { - if (mDrawDelta <= 0) { - return view.mDisplayList; - } - - view.mRecreateDisplayList = (view.mPrivateFlags & View.PFLAG_INVALIDATED) - == View.PFLAG_INVALIDATED; - view.mPrivateFlags &= ~View.PFLAG_INVALIDATED; - - long buildDisplayListStartTime = startBuildDisplayListProfiling(); - canvas.clearLayerUpdates(); - - Trace.traceBegin(Trace.TRACE_TAG_VIEW, "getDisplayList"); - DisplayList displayList = view.getDisplayList(); - Trace.traceEnd(Trace.TRACE_TAG_VIEW); - - endBuildDisplayListProfiling(buildDisplayListStartTime); - - return displayList; - } - - abstract void drawProfileData(View.AttachInfo attachInfo); - - private Rect beginFrame(HardwareCanvas canvas, Rect dirty, int surfaceState) { - // We had to change the current surface and/or context, redraw everything - if (surfaceState == SURFACE_STATE_UPDATED) { - dirty = null; - beginFrame(null); - } else { - int[] size = mSurfaceSize; - beginFrame(size); - - if (size[1] != mHeight || size[0] != mWidth) { - mWidth = size[0]; - mHeight = size[1]; - - canvas.setViewport(mWidth, mHeight); - - dirty = null; - } - } - - if (mDebugDataProvider != null) dirty = null; - - return dirty; - } - - private long startBuildDisplayListProfiling() { - if (mProfileEnabled) { - mProfileCurrentFrame += PROFILE_FRAME_DATA_COUNT; - if (mProfileCurrentFrame >= mProfileData.length) { - mProfileCurrentFrame = 0; - } - - return System.nanoTime(); - } - return 0; - } - - private void endBuildDisplayListProfiling(long getDisplayListStartTime) { - if (mProfileEnabled) { - long now = System.nanoTime(); - float total = (now - getDisplayListStartTime) * 0.000001f; - //noinspection PointlessArithmeticExpression - mProfileData[mProfileCurrentFrame] = total; - } - } - - private int prepareFrame(Rect dirty) { - int status; - Trace.traceBegin(Trace.TRACE_TAG_VIEW, "prepareFrame"); - try { - status = onPreDraw(dirty); - } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIEW); - } - return status; - } - - private int drawDisplayList(View.AttachInfo attachInfo, HardwareCanvas canvas, - DisplayList displayList, int status) { - - long drawDisplayListStartTime = 0; - if (mProfileEnabled) { - drawDisplayListStartTime = System.nanoTime(); - } - - Trace.traceBegin(Trace.TRACE_TAG_VIEW, "drawDisplayList"); - try { - status |= canvas.drawDisplayList(displayList, mRedrawClip, - DisplayList.FLAG_CLIP_CHILDREN); - } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIEW); - } - - if (mProfileEnabled) { - long now = System.nanoTime(); - float total = (now - drawDisplayListStartTime) * 0.000001f; - mProfileData[mProfileCurrentFrame + 1] = total; - } - - handleFunctorStatus(attachInfo, status); - return status; - } - - private void swapBuffers(int status) { - if ((status & DisplayList.STATUS_DREW) == DisplayList.STATUS_DREW) { - long eglSwapBuffersStartTime = 0; - if (mProfileEnabled) { - eglSwapBuffersStartTime = System.nanoTime(); - } - - sEgl.eglSwapBuffers(sEglDisplay, mEglSurface); - - if (mProfileEnabled) { - long now = System.nanoTime(); - float total = (now - eglSwapBuffersStartTime) * 0.000001f; - mProfileData[mProfileCurrentFrame + 2] = total; - } - - checkEglErrors(); - } - } - - private void debugDirtyRegions(Rect dirty, HardwareCanvas canvas) { - if (mDebugDirtyRegions) { - if (mDebugPaint == null) { - mDebugPaint = new Paint(); - mDebugPaint.setColor(0x7fff0000); - } - - if (dirty != null && (mFrameCount & 1) == 0) { - canvas.drawRect(dirty, mDebugPaint); - } - } - } - - private void handleFunctorStatus(View.AttachInfo attachInfo, int status) { - // If the draw flag is set, functors will be invoked while executing - // the tree of display lists - if ((status & DisplayList.STATUS_DRAW) != 0) { - if (mRedrawClip.isEmpty()) { - attachInfo.mViewRootImpl.invalidate(); - } else { - attachInfo.mViewRootImpl.invalidateChildInParent(null, mRedrawClip); - mRedrawClip.setEmpty(); - } - } - - if ((status & DisplayList.STATUS_INVOKE) != 0 || - attachInfo.mHandler.hasCallbacks(mFunctorsRunnable)) { - attachInfo.mHandler.removeCallbacks(mFunctorsRunnable); - mFunctorsRunnable.attachInfo = attachInfo; - attachInfo.mHandler.postDelayed(mFunctorsRunnable, FUNCTOR_PROCESS_DELAY); - } - } - - @Override - void detachFunctor(long functor) { - if (mCanvas != null) { - mCanvas.detachFunctor(functor); - } - } - - @Override - boolean attachFunctor(View.AttachInfo attachInfo, long functor) { - if (mCanvas != null) { - mCanvas.attachFunctor(functor); - mFunctorsRunnable.attachInfo = attachInfo; - attachInfo.mHandler.removeCallbacks(mFunctorsRunnable); - attachInfo.mHandler.postDelayed(mFunctorsRunnable, 0); - return true; - } - return false; - } - - /** - * Ensures the current EGL context and surface are the ones we expect. - * This method throws an IllegalStateException if invoked from a thread - * that did not initialize EGL. - * - * @return {@link #SURFACE_STATE_ERROR} if the correct EGL context cannot be made current, - * {@link #SURFACE_STATE_UPDATED} if the EGL context was changed or - * {@link #SURFACE_STATE_SUCCESS} if the EGL context was the correct one - * - * @see #checkRenderContextUnsafe() - */ - int checkRenderContext() { - if (mEglThread != Thread.currentThread()) { - throw new IllegalStateException("Hardware acceleration can only be used with a " + - "single UI thread.\nOriginal thread: " + mEglThread + "\n" + - "Current thread: " + Thread.currentThread()); - } - - return checkRenderContextUnsafe(); - } - - /** - * Ensures the current EGL context and surface are the ones we expect. - * This method does not check the current thread. - * - * @return {@link #SURFACE_STATE_ERROR} if the correct EGL context cannot be made current, - * {@link #SURFACE_STATE_UPDATED} if the EGL context was changed or - * {@link #SURFACE_STATE_SUCCESS} if the EGL context was the correct one - * - * @see #checkRenderContext() - */ - private int checkRenderContextUnsafe() { - if (!mEglSurface.equals(sEgl.eglGetCurrentSurface(EGL_DRAW)) || - !mEglContext.equals(sEgl.eglGetCurrentContext())) { - if (!sEgl.eglMakeCurrent(sEglDisplay, mEglSurface, mEglSurface, mEglContext)) { - Log.e(LOG_TAG, "eglMakeCurrent failed " + - GLUtils.getEGLErrorString(sEgl.eglGetError())); - fallback(true); - return SURFACE_STATE_ERROR; - } else { - if (mUpdateDirtyRegions) { - enableDirtyRegions(); - mUpdateDirtyRegions = false; - } - return SURFACE_STATE_UPDATED; - } - } - return SURFACE_STATE_SUCCESS; - } - - private static int dpToPx(int dp, float density) { - return (int) (dp * density + 0.5f); - } - - class DrawPerformanceDataProvider extends GraphDataProvider { - private final int mGraphType; - - private int mVerticalUnit; - private int mHorizontalUnit; - private int mHorizontalMargin; - private int mThresholdStroke; - - DrawPerformanceDataProvider(int graphType) { - mGraphType = graphType; - } - - @Override - void prepare(DisplayMetrics metrics) { - final float density = metrics.density; - - mVerticalUnit = dpToPx(PROFILE_DRAW_DP_PER_MS, density); - mHorizontalUnit = dpToPx(PROFILE_DRAW_WIDTH, density); - mHorizontalMargin = dpToPx(PROFILE_DRAW_MARGIN, density); - mThresholdStroke = dpToPx(PROFILE_DRAW_THRESHOLD_STROKE_WIDTH, density); - } - - @Override - int getGraphType() { - return mGraphType; - } - - @Override - int getVerticalUnitSize() { - return mVerticalUnit; - } - - @Override - int getHorizontalUnitSize() { - return mHorizontalUnit; - } - - @Override - int getHorizontaUnitMargin() { - return mHorizontalMargin; - } - - @Override - float[] getData() { - return mProfileData; - } - - @Override - float getThreshold() { - return 16; - } - - @Override - int getFrameCount() { - return mProfileData.length / PROFILE_FRAME_DATA_COUNT; - } - - @Override - int getElementCount() { - return PROFILE_FRAME_DATA_COUNT; - } - - @Override - int getCurrentFrame() { - return mProfileCurrentFrame / PROFILE_FRAME_DATA_COUNT; - } - - @Override - void setupGraphPaint(Paint paint, int elementIndex) { - paint.setColor(PROFILE_DRAW_COLORS[elementIndex]); - if (mGraphType == GRAPH_TYPE_LINES) paint.setStrokeWidth(mThresholdStroke); - } - - @Override - void setupThresholdPaint(Paint paint) { - paint.setColor(PROFILE_DRAW_THRESHOLD_COLOR); - paint.setStrokeWidth(mThresholdStroke); - } - - @Override - void setupCurrentFramePaint(Paint paint) { - paint.setColor(PROFILE_DRAW_CURRENT_FRAME_COLOR); - if (mGraphType == GRAPH_TYPE_LINES) paint.setStrokeWidth(mThresholdStroke); - } - } - } - - /** - * Hardware renderer using OpenGL ES 2.0. - */ - static class Gl20Renderer extends GlRenderer { - private GLES20Canvas mGlCanvas; - - private DisplayMetrics mDisplayMetrics; - - private static EGLSurface sPbuffer; - private static final Object[] sPbufferLock = new Object[0]; - - static class Gl20RendererEglContext extends ManagedEGLContext { - final Handler mHandler = new Handler(); - - public Gl20RendererEglContext(EGLContext context) { - super(context); - } - - @Override - public void onTerminate(final EGLContext eglContext) { - // Make sure we do this on the correct thread. - if (mHandler.getLooper() != Looper.myLooper()) { - mHandler.post(new Runnable() { - @Override - public void run() { - onTerminate(eglContext); - } - }); - return; - } - - synchronized (sEglLock) { - if (sEgl == null) return; - - if (EGLImpl.getInitCount(sEglDisplay) == 1) { - usePbufferSurface(eglContext); - GLES20Canvas.terminateCaches(); - - sEgl.eglDestroyContext(sEglDisplay, eglContext); - sEglContextStorage.set(null); - sEglContextStorage.remove(); - - sEgl.eglDestroySurface(sEglDisplay, sPbuffer); - sEgl.eglMakeCurrent(sEglDisplay, EGL_NO_SURFACE, - EGL_NO_SURFACE, EGL_NO_CONTEXT); - - sEgl.eglReleaseThread(); - sEgl.eglTerminate(sEglDisplay); - - sEgl = null; - sEglDisplay = null; - sEglConfig = null; - sPbuffer = null; - } - } - } - } - - Gl20Renderer(boolean translucent) { - super(2, translucent); - } - - @Override - HardwareCanvas createCanvas() { - return mGlCanvas = new GLES20Canvas(mTranslucent); - } - - @Override - ManagedEGLContext createManagedContext(EGLContext eglContext) { - return new Gl20Renderer.Gl20RendererEglContext(mEglContext); - } - - @Override - int[] getConfig(boolean dirtyRegions) { - //noinspection PointlessBooleanExpression,ConstantConditions - final int stencilSize = GLES20Canvas.getStencilSize(); - final int swapBehavior = dirtyRegions ? EGL14.EGL_SWAP_BEHAVIOR_PRESERVED_BIT : 0; - - return new int[] { - EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, - EGL_RED_SIZE, 8, - EGL_GREEN_SIZE, 8, - EGL_BLUE_SIZE, 8, - EGL_ALPHA_SIZE, 8, - EGL_DEPTH_SIZE, 0, - EGL_CONFIG_CAVEAT, EGL_NONE, - EGL_STENCIL_SIZE, stencilSize, - EGL_SURFACE_TYPE, EGL_WINDOW_BIT | swapBehavior, - EGL_NONE - }; - } - - @Override - void initCaches() { - if (GLES20Canvas.initCaches()) { - // Caches were (re)initialized, rebind atlas - initAtlas(); - } - } - - @Override - void initAtlas() { - IBinder binder = ServiceManager.getService("assetatlas"); - if (binder == null) return; - - IAssetAtlas atlas = IAssetAtlas.Stub.asInterface(binder); - try { - if (atlas.isCompatible(android.os.Process.myPpid())) { - GraphicBuffer buffer = atlas.getBuffer(); - if (buffer != null) { - long[] map = atlas.getMap(); - if (map != null) { - GLES20Canvas.initAtlas(buffer, map); - } - // If IAssetAtlas is not the same class as the IBinder - // we are using a remote service and we can safely - // destroy the graphic buffer - if (atlas.getClass() != binder.getClass()) { - buffer.destroy(); - } - } - } - } catch (RemoteException e) { - Log.w(LOG_TAG, "Could not acquire atlas", e); - } - } - - @Override - boolean canDraw() { - return super.canDraw() && mGlCanvas != null; - } - - @Override - int onPreDraw(Rect dirty) { - return mGlCanvas.onPreDraw(dirty); - } - - @Override - void onPostDraw() { - mGlCanvas.onPostDraw(); - } - - @Override - void drawProfileData(View.AttachInfo attachInfo) { - if (mDebugDataProvider != null) { - final GraphDataProvider provider = mDebugDataProvider; - initProfileDrawData(attachInfo, provider); - - final int height = provider.getVerticalUnitSize(); - final int margin = provider.getHorizontaUnitMargin(); - final int width = provider.getHorizontalUnitSize(); - - int x = 0; - int count = 0; - int current = 0; - - final float[] data = provider.getData(); - final int elementCount = provider.getElementCount(); - final int graphType = provider.getGraphType(); - - int totalCount = provider.getFrameCount() * elementCount; - if (graphType == GraphDataProvider.GRAPH_TYPE_LINES) { - totalCount -= elementCount; - } - - for (int i = 0; i < totalCount; i += elementCount) { - if (data[i] < 0.0f) break; - - int index = count * 4; - if (i == provider.getCurrentFrame() * elementCount) current = index; - - x += margin; - int x2 = x + width; - - int y2 = mHeight; - int y1 = (int) (y2 - data[i] * height); - - switch (graphType) { - case GraphDataProvider.GRAPH_TYPE_BARS: { - for (int j = 0; j < elementCount; j++) { - //noinspection MismatchedReadAndWriteOfArray - final float[] r = mProfileShapes[j]; - r[index] = x; - r[index + 1] = y1; - r[index + 2] = x2; - r[index + 3] = y2; - - y2 = y1; - if (j < elementCount - 1) { - y1 = (int) (y2 - data[i + j + 1] * height); - } - } - } break; - case GraphDataProvider.GRAPH_TYPE_LINES: { - for (int j = 0; j < elementCount; j++) { - //noinspection MismatchedReadAndWriteOfArray - final float[] r = mProfileShapes[j]; - r[index] = (x + x2) * 0.5f; - r[index + 1] = index == 0 ? y1 : r[index - 1]; - r[index + 2] = r[index] + width; - r[index + 3] = y1; - - y2 = y1; - if (j < elementCount - 1) { - y1 = (int) (y2 - data[i + j + 1] * height); - } - } - } break; - } - - - x += width; - count++; - } - - x += margin; - - drawGraph(graphType, count); - drawCurrentFrame(graphType, current); - drawThreshold(x, height); - } - } - - private void drawGraph(int graphType, int count) { - for (int i = 0; i < mProfileShapes.length; i++) { - mDebugDataProvider.setupGraphPaint(mProfilePaint, i); - switch (graphType) { - case GraphDataProvider.GRAPH_TYPE_BARS: - mGlCanvas.drawRects(mProfileShapes[i], count * 4, mProfilePaint); - break; - case GraphDataProvider.GRAPH_TYPE_LINES: - mGlCanvas.drawLines(mProfileShapes[i], 0, count * 4, mProfilePaint); - break; - } - } - } - - private void drawCurrentFrame(int graphType, int index) { - if (index >= 0) { - mDebugDataProvider.setupCurrentFramePaint(mProfilePaint); - switch (graphType) { - case GraphDataProvider.GRAPH_TYPE_BARS: - mGlCanvas.drawRect(mProfileShapes[2][index], mProfileShapes[2][index + 1], - mProfileShapes[2][index + 2], mProfileShapes[0][index + 3], - mProfilePaint); - break; - case GraphDataProvider.GRAPH_TYPE_LINES: - mGlCanvas.drawLine(mProfileShapes[2][index], mProfileShapes[2][index + 1], - mProfileShapes[2][index], mHeight, mProfilePaint); - break; - } - } - } - - private void drawThreshold(int x, int height) { - float threshold = mDebugDataProvider.getThreshold(); - if (threshold > 0.0f) { - mDebugDataProvider.setupThresholdPaint(mProfilePaint); - int y = (int) (mHeight - threshold * height); - mGlCanvas.drawLine(0.0f, y, x, y, mProfilePaint); - } - } - - private void initProfileDrawData(View.AttachInfo attachInfo, GraphDataProvider provider) { - if (mProfileShapes == null) { - final int elementCount = provider.getElementCount(); - final int frameCount = provider.getFrameCount(); - - mProfileShapes = new float[elementCount][]; - for (int i = 0; i < elementCount; i++) { - mProfileShapes[i] = new float[frameCount * 4]; - } - - mProfilePaint = new Paint(); - } - - mProfilePaint.reset(); - if (provider.getGraphType() == GraphDataProvider.GRAPH_TYPE_LINES) { - mProfilePaint.setAntiAlias(true); - } - - if (mDisplayMetrics == null) { - mDisplayMetrics = new DisplayMetrics(); - } - - attachInfo.mDisplay.getMetrics(mDisplayMetrics); - provider.prepare(mDisplayMetrics); - } - - @Override - void destroy(boolean full) { - try { - super.destroy(full); - } finally { - if (full && mGlCanvas != null) { - mGlCanvas = null; - } - } - } - - @Override - void pushLayerUpdate(HardwareLayer layer) { - mGlCanvas.pushLayerUpdate(layer); - } - - @Override - void cancelLayerUpdate(HardwareLayer layer) { - mGlCanvas.cancelLayerUpdate(layer); - } - - @Override - void flushLayerUpdates() { - mGlCanvas.flushLayerUpdates(); - } - - @Override - public DisplayList createDisplayList(String name) { - return new GLES20DisplayList(name); - } - - @Override - HardwareLayer createHardwareLayer(boolean isOpaque) { - return new GLES20TextureLayer(isOpaque); - } - - @Override - public HardwareLayer createHardwareLayer(int width, int height, boolean isOpaque) { - return new GLES20RenderLayer(width, height, isOpaque); - } - - @Override - void countOverdraw(HardwareCanvas canvas) { - ((GLES20Canvas) canvas).setCountOverdrawEnabled(true); - } - - @Override - float getOverdraw(HardwareCanvas canvas) { - return ((GLES20Canvas) canvas).getOverdraw(); - } - - @Override - public SurfaceTexture createSurfaceTexture(HardwareLayer layer) { - return ((GLES20TextureLayer) layer).getSurfaceTexture(); - } - - @Override - void setSurfaceTexture(HardwareLayer layer, SurfaceTexture surfaceTexture) { - ((GLES20TextureLayer) layer).setSurfaceTexture(surfaceTexture); - } - - @Override - boolean safelyRun(Runnable action) { - boolean needsContext = !isEnabled() || checkRenderContext() == SURFACE_STATE_ERROR; - - if (needsContext) { - Gl20RendererEglContext managedContext = - (Gl20RendererEglContext) sEglContextStorage.get(); - if (managedContext == null) return false; - usePbufferSurface(managedContext.getContext()); - } - - try { - action.run(); - } finally { - if (needsContext) { - sEgl.eglMakeCurrent(sEglDisplay, EGL_NO_SURFACE, - EGL_NO_SURFACE, EGL_NO_CONTEXT); - } - } - - return true; - } - - @Override - void destroyLayers(final View view) { - if (view != null) { - safelyRun(new Runnable() { - @Override - public void run() { - if (mCanvas != null) { - mCanvas.clearLayerUpdates(); - } - destroyHardwareLayer(view); - GLES20Canvas.flushCaches(GLES20Canvas.FLUSH_CACHES_LAYERS); - } - }); - } - } - - private static void destroyHardwareLayer(View view) { - view.destroyLayer(true); - - if (view instanceof ViewGroup) { - ViewGroup group = (ViewGroup) view; - - int count = group.getChildCount(); - for (int i = 0; i < count; i++) { - destroyHardwareLayer(group.getChildAt(i)); - } - } - } - - @Override - void destroyHardwareResources(final View view) { - if (view != null) { - safelyRun(new Runnable() { - @Override - public void run() { - if (mCanvas != null) { - mCanvas.clearLayerUpdates(); - } - destroyResources(view); - GLES20Canvas.flushCaches(GLES20Canvas.FLUSH_CACHES_LAYERS); - } - }); - } - } - - private static void destroyResources(View view) { - view.destroyHardwareResources(); - - if (view instanceof ViewGroup) { - ViewGroup group = (ViewGroup) view; - - int count = group.getChildCount(); - for (int i = 0; i < count; i++) { - destroyResources(group.getChildAt(i)); - } - } - } - - static HardwareRenderer create(boolean translucent) { - if (GLES20Canvas.isAvailable()) { - return new Gl20Renderer(translucent); - } - return null; - } - - static void startTrimMemory(int level) { - if (sEgl == null || sEglConfig == null) return; - - Gl20RendererEglContext managedContext = - (Gl20RendererEglContext) sEglContextStorage.get(); - // We do not have OpenGL objects - if (managedContext == null) { - return; - } else { - usePbufferSurface(managedContext.getContext()); - } - - if (level >= ComponentCallbacks2.TRIM_MEMORY_COMPLETE) { - GLES20Canvas.flushCaches(GLES20Canvas.FLUSH_CACHES_FULL); - } else if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) { - GLES20Canvas.flushCaches(GLES20Canvas.FLUSH_CACHES_MODERATE); - } - } - - static void endTrimMemory() { - if (sEgl != null && sEglDisplay != null) { - sEgl.eglMakeCurrent(sEglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); - } - } - - private static void usePbufferSurface(EGLContext eglContext) { - synchronized (sPbufferLock) { - // Create a temporary 1x1 pbuffer so we have a context - // to clear our OpenGL objects - if (sPbuffer == null) { - sPbuffer = sEgl.eglCreatePbufferSurface(sEglDisplay, sEglConfig, new int[] { - EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE - }); - } - } - sEgl.eglMakeCurrent(sEglDisplay, sPbuffer, sPbuffer, eglContext); - } - } } diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index c92a104..7d13399 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -27,7 +27,6 @@ import android.graphics.Rect; import android.os.Bundle; import android.os.IRemoteCallback; import android.view.IApplicationToken; -import android.view.IMagnificationCallbacks; import android.view.IOnKeyguardExitResult; import android.view.IRotationWatcher; import android.view.IWindowSession; @@ -38,6 +37,7 @@ import android.view.MotionEvent; import android.view.InputChannel; import android.view.InputDevice; import android.view.IInputFilter; +import android.view.WindowContentFrameStats; /** * System private interface to the window manager. @@ -197,7 +197,7 @@ interface IWindowManager void thawRotation(); /** - * Gets whether the rotation is frozen. + * Gets whether the rotation is frozen. * * @return Whether the rotation is frozen. */ @@ -231,55 +231,28 @@ interface IWindowManager void lockNow(in Bundle options); /** - * Gets the token for the focused window. - */ - IBinder getFocusedWindowToken(); - - /** - * Sets an input filter for manipulating the input event stream. - */ - void setInputFilter(in IInputFilter filter); - - /** - * Gets the frame of a window given its token. - */ - void getWindowFrame(IBinder token, out Rect outFrame); - - /** * Device is in safe mode. */ boolean isSafeModeEnabled(); /** - * Sets the display magnification callbacks. These callbacks notify - * the client for contextual changes related to display magnification. - * - * @param callbacks The magnification callbacks. - */ - void setMagnificationCallbacks(IMagnificationCallbacks callbacks); - - /** - * Sets the magnification spec to be applied to all windows that can be - * magnified. - * - * @param spec The current magnification spec. + * Enables the screen if all conditions are met. */ - void setMagnificationSpec(in MagnificationSpec spec); + void enableScreenIfNeeded(); /** - * Gets the magnification spec for a window given its token. If the - * window has a compatibility scale it is also folded in the returned - * magnification spec. + * Clears the frame statistics for a given window. * - * @param windowToken The unique window token. - * @return The magnification spec if such or null. + * @param token The window token. + * @return Whether the frame statistics were cleared. */ - MagnificationSpec getCompatibleMagnificationSpecForWindow(in IBinder windowToken); + boolean clearWindowContentFrameStats(IBinder token); /** - * Sets the current touch exploration state. + * Gets the content frame statistics for a given window. * - * @param enabled Whether touch exploration is enabled. + * @param token The window token. + * @return The frame statistics or null if the window does not exist. */ - void setTouchExplorationEnabled(boolean enabled); + WindowContentFrameStats getWindowContentFrameStats(IBinder token); } diff --git a/core/java/android/view/InputQueue.java b/core/java/android/view/InputQueue.java index d5cec49..aebc601 100644 --- a/core/java/android/view/InputQueue.java +++ b/core/java/android/view/InputQueue.java @@ -18,7 +18,6 @@ package android.view; import dalvik.system.CloseGuard; -import android.os.Handler; import android.os.Looper; import android.os.MessageQueue; import android.util.Pools.Pool; diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java index 7b389c0..c3f429c 100644 --- a/core/java/android/view/KeyEvent.java +++ b/core/java/android/view/KeyEvent.java @@ -20,7 +20,6 @@ import android.os.Parcel; import android.os.Parcelable; import android.text.method.MetaKeyKeyListener; import android.util.Log; -import android.util.Slog; import android.util.SparseArray; import android.util.SparseIntArray; import android.view.KeyCharacterMap; @@ -1178,20 +1177,20 @@ public class KeyEvent extends InputEvent implements Parcelable { * This mask is set if the key event was generated by a software keyboard. */ public static final int FLAG_SOFT_KEYBOARD = 0x2; - + /** * This mask is set if we don't want the key event to cause us to leave * touch mode. */ public static final int FLAG_KEEP_TOUCH_MODE = 0x4; - + /** * This mask is set if an event was known to come from a trusted part * of the system. That is, the event is known to come from the user, * and could not have been spoofed by a third party component. */ public static final int FLAG_FROM_SYSTEM = 0x8; - + /** * This mask is used for compatibility, to identify enter keys that are * coming from an IME whose enter key has been auto-labelled "next" or @@ -1200,7 +1199,7 @@ public class KeyEvent extends InputEvent implements Parcelable { * receiving them. */ public static final int FLAG_EDITOR_ACTION = 0x10; - + /** * When associated with up key events, this indicates that the key press * has been canceled. Typically this is used with virtual touch screen @@ -1209,29 +1208,29 @@ public class KeyEvent extends InputEvent implements Parcelable { * event and should not perform the action normally associated with the * key. Note that for this to work, the application can not perform an * action for a key until it receives an up or the long press timeout has - * expired. + * expired. */ public static final int FLAG_CANCELED = 0x20; - + /** * This key event was generated by a virtual (on-screen) hard key area. * Typically this is an area of the touchscreen, outside of the regular * display, dedicated to "hardware" buttons. */ public static final int FLAG_VIRTUAL_HARD_KEY = 0x40; - + /** * This flag is set for the first key repeat that occurs after the * long press timeout. */ public static final int FLAG_LONG_PRESS = 0x80; - + /** * Set when a key event has {@link #FLAG_CANCELED} set because a long - * press action was executed while it was down. + * press action was executed while it was down. */ public static final int FLAG_CANCELED_LONG_PRESS = 0x100; - + /** * Set for {@link #ACTION_UP} when this event's key code is still being * tracked from its initial down. That is, somebody requested that tracking @@ -1288,7 +1287,7 @@ public class KeyEvent extends InputEvent implements Parcelable { public static int getDeadChar(int accent, int c) { return KeyCharacterMap.getDeadChar(accent, c); } - + static final boolean DEBUG = false; static final String TAG = "KeyEvent"; @@ -1318,10 +1317,10 @@ public class KeyEvent extends InputEvent implements Parcelable { * KeyEvent.startTracking()} to have the framework track the event * through its {@link #onKeyUp(int, KeyEvent)} and also call your * {@link #onKeyLongPress(int, KeyEvent)} if it occurs. - * + * * @param keyCode The value in event.getKeyCode(). * @param event Description of the key event. - * + * * @return If you handled the event, return true. If you want to allow * the event to be handled by the next receiver, return false. */ @@ -1334,10 +1333,10 @@ public class KeyEvent extends InputEvent implements Parcelable { * order to receive this callback, someone in the event change * <em>must</em> return true from {@link #onKeyDown} <em>and</em> * call {@link KeyEvent#startTracking()} on the event. - * + * * @param keyCode The value in event.getKeyCode(). * @param event Description of the key event. - * + * * @return If you handled the event, return true. If you want to allow * the event to be handled by the next receiver, return false. */ @@ -1345,10 +1344,10 @@ public class KeyEvent extends InputEvent implements Parcelable { /** * Called when a key up event has occurred. - * + * * @param keyCode The value in event.getKeyCode(). * @param event Description of the key event. - * + * * @return If you handled the event, return true. If you want to allow * the event to be handled by the next receiver, return false. */ @@ -1357,11 +1356,11 @@ public class KeyEvent extends InputEvent implements Parcelable { /** * Called when multiple down/up pairs of the same key have occurred * in a row. - * + * * @param keyCode The value in event.getKeyCode(). * @param count Number of pairs as returned by event.getRepeatCount(). * @param event Description of the key event. - * + * * @return If you handled the event, return true. If you want to allow * the event to be handled by the next receiver, return false. */ @@ -1377,7 +1376,7 @@ public class KeyEvent extends InputEvent implements Parcelable { /** * Create a new key event. - * + * * @param action Action code: either {@link #ACTION_DOWN}, * {@link #ACTION_UP}, or {@link #ACTION_MULTIPLE}. * @param code The key code. @@ -1391,7 +1390,7 @@ public class KeyEvent extends InputEvent implements Parcelable { /** * Create a new key event. - * + * * @param downTime The time (in {@link android.os.SystemClock#uptimeMillis}) * at which this key code originally went down. * @param eventTime The time (in {@link android.os.SystemClock#uptimeMillis}) @@ -1414,7 +1413,7 @@ public class KeyEvent extends InputEvent implements Parcelable { /** * Create a new key event. - * + * * @param downTime The time (in {@link android.os.SystemClock#uptimeMillis}) * at which this key code originally went down. * @param eventTime The time (in {@link android.os.SystemClock#uptimeMillis}) @@ -1439,7 +1438,7 @@ public class KeyEvent extends InputEvent implements Parcelable { /** * Create a new key event. - * + * * @param downTime The time (in {@link android.os.SystemClock#uptimeMillis}) * at which this key code originally went down. * @param eventTime The time (in {@link android.os.SystemClock#uptimeMillis}) @@ -1468,7 +1467,7 @@ public class KeyEvent extends InputEvent implements Parcelable { /** * Create a new key event. - * + * * @param downTime The time (in {@link android.os.SystemClock#uptimeMillis}) * at which this key code originally went down. * @param eventTime The time (in {@link android.os.SystemClock#uptimeMillis}) @@ -1499,7 +1498,7 @@ public class KeyEvent extends InputEvent implements Parcelable { /** * Create a new key event. - * + * * @param downTime The time (in {@link android.os.SystemClock#uptimeMillis}) * at which this key code originally went down. * @param eventTime The time (in {@link android.os.SystemClock#uptimeMillis}) @@ -1535,7 +1534,7 @@ public class KeyEvent extends InputEvent implements Parcelable { * action, repeat count and source will automatically be set to * {@link #KEYCODE_UNKNOWN}, {@link #ACTION_MULTIPLE}, 0, and * {@link InputDevice#SOURCE_KEYBOARD} for you. - * + * * @param time The time (in {@link android.os.SystemClock#uptimeMillis}) * at which this event occured. * @param characters The string of characters. @@ -1573,10 +1572,10 @@ public class KeyEvent extends InputEvent implements Parcelable { /** * Copy an existing key event, modifying its time and repeat count. - * + * * @deprecated Use {@link #changeTimeRepeat(KeyEvent, long, int)} * instead. - * + * * @param origEvent The existing event to be copied. * @param eventTime The new event time * (in {@link android.os.SystemClock#uptimeMillis}) of the event. @@ -1692,7 +1691,7 @@ public class KeyEvent extends InputEvent implements Parcelable { /** * Create a new key event that is the same as the given one, but whose * event time and repeat count are replaced with the given value. - * + * * @param event The existing event to be copied. This is not modified. * @param eventTime The new event time * (in {@link android.os.SystemClock#uptimeMillis}) of the event. @@ -1702,11 +1701,11 @@ public class KeyEvent extends InputEvent implements Parcelable { int newRepeat) { return new KeyEvent(event, eventTime, newRepeat); } - + /** * Create a new key event that is the same as the given one, but whose * event time and repeat count are replaced with the given value. - * + * * @param event The existing event to be copied. This is not modified. * @param eventTime The new event time * (in {@link android.os.SystemClock#uptimeMillis}) of the event. @@ -1722,10 +1721,10 @@ public class KeyEvent extends InputEvent implements Parcelable { ret.mFlags = newFlags; return ret; } - + /** * Copy an existing key event, modifying its action. - * + * * @param origEvent The existing event to be copied. * @param action The new action code of the event. */ @@ -1747,18 +1746,18 @@ public class KeyEvent extends InputEvent implements Parcelable { /** * Create a new key event that is the same as the given one, but whose * action is replaced with the given value. - * + * * @param event The existing event to be copied. This is not modified. * @param action The new action code of the event. */ public static KeyEvent changeAction(KeyEvent event, int action) { return new KeyEvent(event, action); } - + /** * Create a new key event that is the same as the given one, but whose * flags are replaced with the given value. - * + * * @param event The existing event to be copied. This is not modified. * @param flags The new flags constant. */ @@ -1783,7 +1782,7 @@ public class KeyEvent extends InputEvent implements Parcelable { /** * Don't use in new code, instead explicitly check * {@link #getAction()}. - * + * * @return If the action is ACTION_DOWN, returns true; else false. * * @deprecated @@ -1795,7 +1794,7 @@ public class KeyEvent extends InputEvent implements Parcelable { /** * Is this a system key? System keys can not be used for menu shortcuts. - * + * * TODO: this information should come from a table somewhere. * TODO: should the dpad keys be here? arguably, because they also shouldn't be menu shortcuts */ @@ -1851,16 +1850,13 @@ public class KeyEvent extends InputEvent implements Parcelable { } } - /** - * Returns true if the key event should be treated as a confirming action. - * @return True for a confirmation key, such as {@link #KEYCODE_DPAD_CENTER}, - * {@link #KEYCODE_ENTER}, or {@link #KEYCODE_BUTTON_A}. + /** Whether key will, by default, trigger a click on the focused view. + * @hide */ - public final boolean isConfirmKey() { - switch (mKeyCode) { + public static final boolean isConfirmKey(int keyCode) { + switch (keyCode) { case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_ENTER: - case KeyEvent.KEYCODE_BUTTON_A: return true; default: return false; @@ -1868,19 +1864,27 @@ public class KeyEvent extends InputEvent implements Parcelable { } /** - * Returns true if the key event should be treated as a cancelling action. - * @return True for a cancellation key, such as {@link #KEYCODE_ESCAPE}, - * {@link #KEYCODE_BACK}, or {@link #KEYCODE_BUTTON_B}. + * Whether this key is a media key, which can be send to apps that are + * interested in media key events. + * + * @hide */ - public final boolean isCancelKey() { - switch (mKeyCode) { - case KeyEvent.KEYCODE_BUTTON_B: - case KeyEvent.KEYCODE_ESCAPE: - case KeyEvent.KEYCODE_BACK: + public static final boolean isMediaKey(int keyCode) { + switch (keyCode) { + case KeyEvent.KEYCODE_MEDIA_PLAY: + case KeyEvent.KEYCODE_MEDIA_PAUSE: + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + case KeyEvent.KEYCODE_MUTE: + case KeyEvent.KEYCODE_HEADSETHOOK: + case KeyEvent.KEYCODE_MEDIA_STOP: + case KeyEvent.KEYCODE_MEDIA_NEXT: + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + case KeyEvent.KEYCODE_MEDIA_REWIND: + case KeyEvent.KEYCODE_MEDIA_RECORD: + case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: return true; - default: - return false; } + return false; } /** {@inheritDoc} */ @@ -2352,7 +2356,7 @@ public class KeyEvent extends InputEvent implements Parcelable { /** * Retrieve the action of this key event. May be either * {@link #ACTION_DOWN}, {@link #ACTION_UP}, or {@link #ACTION_MULTIPLE}. - * + * * @return The event action: ACTION_DOWN, ACTION_UP, or ACTION_MULTIPLE. */ public final int getAction() { @@ -2366,7 +2370,7 @@ public class KeyEvent extends InputEvent implements Parcelable { public final boolean isCanceled() { return (mFlags&FLAG_CANCELED) != 0; } - + /** * Call this during {@link Callback#onKeyDown} to have the system track * the key through its final up (possibly including a long press). Note @@ -2377,7 +2381,7 @@ public class KeyEvent extends InputEvent implements Parcelable { public final void startTracking() { mFlags |= FLAG_START_TRACKING; } - + /** * For {@link #ACTION_UP} events, indicates that the event is still being * tracked from its initial down event as per @@ -2386,7 +2390,7 @@ public class KeyEvent extends InputEvent implements Parcelable { public final boolean isTracking() { return (mFlags&FLAG_TRACKING) != 0; } - + /** * For {@link #ACTION_DOWN} events, indicates that the event has been * canceled as per {@link #FLAG_LONG_PRESS}. @@ -2394,11 +2398,11 @@ public class KeyEvent extends InputEvent implements Parcelable { public final boolean isLongPress() { return (mFlags&FLAG_LONG_PRESS) != 0; } - + /** * Retrieve the key code of the key event. This is the physical key that * was pressed, <em>not</em> the Unicode character. - * + * * @return The key code of the event. */ public final int getKeyCode() { @@ -2409,14 +2413,14 @@ public class KeyEvent extends InputEvent implements Parcelable { * For the special case of a {@link #ACTION_MULTIPLE} event with key * code of {@link #KEYCODE_UNKNOWN}, this is a raw string of characters * associated with the event. In all other cases it is null. - * + * * @return Returns a String of 1 or more characters associated with * the event. */ public final String getCharacters() { return mCharacters; } - + /** * Retrieve the hardware key id of this key event. These values are not * reliable and vary from device to device. @@ -2433,7 +2437,7 @@ public class KeyEvent extends InputEvent implements Parcelable { * events, this is the number of times the key has repeated with the first * down starting at 0 and counting up from there. For multiple key * events, this is the number of down/up pairs that have occurred. - * + * * @return The number of times the key has repeated. */ public final int getRepeatCount() { @@ -2447,7 +2451,7 @@ public class KeyEvent extends InputEvent implements Parcelable { * Note that when chording keys, this value is the down time of the * most recently pressed key, which may <em>not</em> be the same physical * key of this event. - * + * * @return Returns the most recent key down time, in the * {@link android.os.SystemClock#uptimeMillis} time base */ @@ -2459,7 +2463,7 @@ public class KeyEvent extends InputEvent implements Parcelable { * Retrieve the time this event occurred, * in the {@link android.os.SystemClock#uptimeMillis} time base. * - * @return Returns the time this event occurred, + * @return Returns the time this event occurred, * in the {@link android.os.SystemClock#uptimeMillis} time base. */ @Override @@ -2488,7 +2492,7 @@ public class KeyEvent extends InputEvent implements Parcelable { /** * Renamed to {@link #getDeviceId}. - * + * * @hide * @deprecated use {@link #getDeviceId()} instead. */ @@ -2520,7 +2524,7 @@ public class KeyEvent extends InputEvent implements Parcelable { public char getDisplayLabel() { return getKeyCharacterMap().getDisplayLabel(mKeyCode); } - + /** * Gets the Unicode character generated by the specified key and meta * key state combination. @@ -2543,7 +2547,7 @@ public class KeyEvent extends InputEvent implements Parcelable { public int getUnicodeChar() { return getUnicodeChar(mMetaState); } - + /** * Gets the Unicode character generated by the specified key and meta * key state combination. @@ -2567,7 +2571,7 @@ public class KeyEvent extends InputEvent implements Parcelable { public int getUnicodeChar(int metaState) { return getKeyCharacterMap().get(mKeyCode, metaState); } - + /** * Get the character conversion data for a given key code. * @@ -2582,7 +2586,7 @@ public class KeyEvent extends InputEvent implements Parcelable { public boolean getKeyData(KeyData results) { return getKeyCharacterMap().getKeyData(mKeyCode, results); } - + /** * Gets the first character in the character array that can be generated * by the specified key code. @@ -2597,7 +2601,7 @@ public class KeyEvent extends InputEvent implements Parcelable { public char getMatch(char[] chars) { return getMatch(chars, 0); } - + /** * Gets the first character in the character array that can be generated * by the specified key code. If there are multiple choices, prefers @@ -2610,7 +2614,7 @@ public class KeyEvent extends InputEvent implements Parcelable { public char getMatch(char[] chars, int metaState) { return getKeyCharacterMap().getMatch(mKeyCode, chars, metaState); } - + /** * Gets the number or symbol associated with the key. * <p> @@ -2634,7 +2638,7 @@ public class KeyEvent extends InputEvent implements Parcelable { public char getNumber() { return getKeyCharacterMap().getNumber(mKeyCode); } - + /** * Returns true if this key produces a glyph. * @@ -2643,7 +2647,7 @@ public class KeyEvent extends InputEvent implements Parcelable { public boolean isPrintingKey() { return getKeyCharacterMap().isPrintingKey(mKeyCode); } - + /** * @deprecated Use {@link #dispatch(Callback, DispatcherState, Object)} instead. */ @@ -2651,16 +2655,16 @@ public class KeyEvent extends InputEvent implements Parcelable { public final boolean dispatch(Callback receiver) { return dispatch(receiver, null, null); } - + /** * Deliver this key event to a {@link Callback} interface. If this is * an ACTION_MULTIPLE event and it is not handled, then an attempt will * be made to deliver a single normal event. - * + * * @param receiver The Callback that will be given the event. * @param state State information retained across events. * @param target The target of the dispatch, for use in tracking. - * + * * @return The return value from the Callback method that was called. */ public final boolean dispatch(Callback receiver, DispatcherState state, @@ -2726,7 +2730,7 @@ public class KeyEvent extends InputEvent implements Parcelable { int mDownKeyCode; Object mDownTarget; SparseIntArray mActiveLongPresses = new SparseIntArray(); - + /** * Reset back to initial state. */ @@ -2736,7 +2740,7 @@ public class KeyEvent extends InputEvent implements Parcelable { mDownTarget = null; mActiveLongPresses.clear(); } - + /** * Stop any tracking associated with this target. */ @@ -2747,14 +2751,14 @@ public class KeyEvent extends InputEvent implements Parcelable { mDownTarget = null; } } - + /** * Start tracking the key code associated with the given event. This * can only be called on a key down. It will allow you to see any * long press associated with the key, and will result in * {@link KeyEvent#isTracking} return true on the long press and up * events. - * + * * <p>This is only needed if you are directly dispatching events, rather * than handling them in {@link Callback#onKeyDown}. */ @@ -2767,7 +2771,7 @@ public class KeyEvent extends InputEvent implements Parcelable { mDownKeyCode = event.getKeyCode(); mDownTarget = target; } - + /** * Return true if the key event is for a key code that is currently * being tracked by the dispatcher. @@ -2775,7 +2779,7 @@ public class KeyEvent extends InputEvent implements Parcelable { public boolean isTracking(KeyEvent event) { return mDownKeyCode == event.getKeyCode(); } - + /** * Keep track of the given event's key code as having performed an * action with a long press, so no action should occur on the up. @@ -2785,7 +2789,7 @@ public class KeyEvent extends InputEvent implements Parcelable { public void performedLongPress(KeyEvent event) { mActiveLongPresses.put(event.getKeyCode(), 1); } - + /** * Handle key up event to stop tracking. This resets the dispatcher state, * and updates the key event state based on it. @@ -2940,12 +2944,12 @@ public class KeyEvent extends InputEvent implements Parcelable { return new KeyEvent[size]; } }; - + /** @hide */ public static KeyEvent createFromParcelBody(Parcel in) { return new KeyEvent(in); } - + private KeyEvent(Parcel in) { mDeviceId = in.readInt(); mSource = in.readInt(); diff --git a/core/java/android/view/LayoutInflater.java b/core/java/android/view/LayoutInflater.java index cd905fa..e19bda9 100644 --- a/core/java/android/view/LayoutInflater.java +++ b/core/java/android/view/LayoutInflater.java @@ -90,6 +90,10 @@ public abstract class LayoutInflater { private static final String TAG_INCLUDE = "include"; private static final String TAG_1995 = "blink"; private static final String TAG_REQUEST_FOCUS = "requestFocus"; + private static final String TAG_TAG = "tag"; + + private static final int[] ATTRS_THEME = new int[] { + com.android.internal.R.attr.theme }; /** * Hook to allow clients of the LayoutInflater to restrict the set of Views that are allowed @@ -459,15 +463,10 @@ public abstract class LayoutInflater { + "ViewGroup root and attachToRoot=true"); } - rInflate(parser, root, attrs, false); + rInflate(parser, root, attrs, false, false); } else { // Temp is the root view that was found in the xml - View temp; - if (TAG_1995.equals(name)) { - temp = new BlinkLayout(mContext, attrs); - } else { - temp = createViewFromTag(root, name, attrs); - } + final View temp = createViewFromTag(root, name, attrs, false); ViewGroup.LayoutParams params = null; @@ -489,7 +488,7 @@ public abstract class LayoutInflater { System.out.println("-----> start inflating children"); } // Inflate all children under temp - rInflate(parser, temp, attrs, true); + rInflate(parser, temp, attrs, true, true); if (DEBUG) { System.out.println("-----> done inflating children"); } @@ -670,31 +669,68 @@ public abstract class LayoutInflater { return onCreateView(name, attrs); } - /* - * default visibility so the BridgeInflater can override it. + /** + * Creates a view from a tag name using the supplied attribute set. + * <p> + * If {@code inheritContext} is true and the parent is non-null, the view + * will be inflated in parent view's context. If the view specifies a + * <theme> attribute, the inflation context will be wrapped with the + * specified theme. + * <p> + * Note: Default visibility so the BridgeInflater can override it. */ - View createViewFromTag(View parent, String name, AttributeSet attrs) { + View createViewFromTag(View parent, String name, AttributeSet attrs, boolean inheritContext) { if (name.equals("view")) { name = attrs.getAttributeValue(null, "class"); } + Context viewContext; + if (parent != null && inheritContext) { + viewContext = parent.getContext(); + } else { + viewContext = mContext; + } + + // Apply a theme wrapper, if requested. + final TypedArray ta = viewContext.obtainStyledAttributes(attrs, ATTRS_THEME); + final int themeResId = ta.getResourceId(0, 0); + if (themeResId != 0) { + viewContext = new ContextThemeWrapper(viewContext, themeResId); + } + ta.recycle(); + + if (name.equals(TAG_1995)) { + // Let's party like it's 1995! + return new BlinkLayout(viewContext, attrs); + } + if (DEBUG) System.out.println("******** Creating view: " + name); try { View view; - if (mFactory2 != null) view = mFactory2.onCreateView(parent, name, mContext, attrs); - else if (mFactory != null) view = mFactory.onCreateView(name, mContext, attrs); - else view = null; + if (mFactory2 != null) { + view = mFactory2.onCreateView(parent, name, viewContext, attrs); + } else if (mFactory != null) { + view = mFactory.onCreateView(name, viewContext, attrs); + } else { + view = null; + } if (view == null && mPrivateFactory != null) { - view = mPrivateFactory.onCreateView(parent, name, mContext, attrs); + view = mPrivateFactory.onCreateView(parent, name, viewContext, attrs); } - + if (view == null) { - if (-1 == name.indexOf('.')) { - view = onCreateView(parent, name, attrs); - } else { - view = createView(name, null, attrs); + final Object lastContext = mConstructorArgs[0]; + mConstructorArgs[0] = viewContext; + try { + if (-1 == name.indexOf('.')) { + view = onCreateView(parent, name, attrs); + } else { + view = createView(name, null, attrs); + } + } finally { + mConstructorArgs[0] = lastContext; } } @@ -721,9 +757,14 @@ public abstract class LayoutInflater { /** * Recursive method used to descend down the xml hierarchy and instantiate * views, instantiate their children, and then call onFinishInflate(). + * + * @param inheritContext Whether the root view should be inflated in its + * parent's context. This should be true when called inflating + * child views recursively, or false otherwise. */ void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs, - boolean finishInflate) throws XmlPullParserException, IOException { + boolean finishInflate, boolean inheritContext) throws XmlPullParserException, + IOException { final int depth = parser.getDepth(); int type; @@ -739,24 +780,20 @@ public abstract class LayoutInflater { if (TAG_REQUEST_FOCUS.equals(name)) { parseRequestFocus(parser, parent); + } else if (TAG_TAG.equals(name)) { + parseViewTag(parser, parent, attrs); } else if (TAG_INCLUDE.equals(name)) { if (parser.getDepth() == 0) { throw new InflateException("<include /> cannot be the root element"); } - parseInclude(parser, parent, attrs); + parseInclude(parser, parent, attrs, inheritContext); } else if (TAG_MERGE.equals(name)) { throw new InflateException("<merge /> must be the root element"); - } else if (TAG_1995.equals(name)) { - final View view = new BlinkLayout(mContext, attrs); - final ViewGroup viewGroup = (ViewGroup) parent; - final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs); - rInflate(parser, view, attrs, true); - viewGroup.addView(view, params); } else { - final View view = createViewFromTag(parent, name, attrs); + final View view = createViewFromTag(parent, name, attrs, inheritContext); final ViewGroup viewGroup = (ViewGroup) parent; final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs); - rInflate(parser, view, attrs, true); + rInflate(parser, view, attrs, true, true); viewGroup.addView(view, params); } } @@ -764,10 +801,14 @@ public abstract class LayoutInflater { if (finishInflate) parent.onFinishInflate(); } - private void parseRequestFocus(XmlPullParser parser, View parent) + /** + * Parses a <code><request-focus></code> element and requests focus on + * the containing View. + */ + private void parseRequestFocus(XmlPullParser parser, View view) throws XmlPullParserException, IOException { int type; - parent.requestFocus(); + view.requestFocus(); final int currentDepth = parser.getDepth(); while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > currentDepth) && type != XmlPullParser.END_DOCUMENT) { @@ -775,9 +816,30 @@ public abstract class LayoutInflater { } } - private void parseInclude(XmlPullParser parser, View parent, AttributeSet attrs) + /** + * Parses a <code><tag></code> element and sets a keyed tag on the + * containing View. + */ + private void parseViewTag(XmlPullParser parser, View view, AttributeSet attrs) throws XmlPullParserException, IOException { + int type; + + final TypedArray ta = mContext.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.ViewTag); + final int key = ta.getResourceId(com.android.internal.R.styleable.ViewTag_id, 0); + final CharSequence value = ta.getText(com.android.internal.R.styleable.ViewTag_value); + view.setTag(key, value); + ta.recycle(); + + final int currentDepth = parser.getDepth(); + while (((type = parser.next()) != XmlPullParser.END_TAG || + parser.getDepth() > currentDepth) && type != XmlPullParser.END_DOCUMENT) { + // Empty + } + } + private void parseInclude(XmlPullParser parser, View parent, AttributeSet attrs, + boolean inheritContext) throws XmlPullParserException, IOException { int type; if (parent instanceof ViewGroup) { @@ -812,9 +874,10 @@ public abstract class LayoutInflater { if (TAG_MERGE.equals(childName)) { // Inflate all children. - rInflate(childParser, parent, childAttrs, false); + rInflate(childParser, parent, childAttrs, false, inheritContext); } else { - final View view = createViewFromTag(parent, childName, childAttrs); + final View view = createViewFromTag(parent, childName, childAttrs, + inheritContext); final ViewGroup group = (ViewGroup) parent; // We try to load the layout params set in the <include /> tag. If @@ -837,7 +900,7 @@ public abstract class LayoutInflater { } // Inflate all children. - rInflate(childParser, view, childAttrs, true); + rInflate(childParser, view, childAttrs, true, true); // Attempt to override the included layout's android:id with the // one set on the <include /> tag itself. diff --git a/core/java/android/view/PointerIcon.java b/core/java/android/view/PointerIcon.java index bb7ed41..063a08d 100644 --- a/core/java/android/view/PointerIcon.java +++ b/core/java/android/view/PointerIcon.java @@ -140,7 +140,7 @@ public final class PointerIcon implements Parcelable { if ((resourceId & 0xff000000) == 0x01000000) { icon.mSystemIconResourceId = resourceId; } else { - icon.loadResource(context.getResources(), resourceId); + icon.loadResource(context, context.getResources(), resourceId); } return icon; } @@ -198,7 +198,7 @@ public final class PointerIcon implements Parcelable { } PointerIcon icon = new PointerIcon(STYLE_CUSTOM); - icon.loadResource(resources, resourceId); + icon.loadResource(null, resources, resourceId); return icon; } @@ -224,7 +224,7 @@ public final class PointerIcon implements Parcelable { PointerIcon result = new PointerIcon(mStyle); result.mSystemIconResourceId = mSystemIconResourceId; - result.loadResource(context.getResources(), mSystemIconResourceId); + result.loadResource(context, context.getResources(), mSystemIconResourceId); return result; } @@ -373,7 +373,7 @@ public final class PointerIcon implements Parcelable { return true; } - private void loadResource(Resources resources, int resourceId) { + private void loadResource(Context context, Resources resources, int resourceId) { XmlResourceParser parser = resources.getXml(resourceId); final int bitmapRes; final float hotSpotX; @@ -397,7 +397,12 @@ public final class PointerIcon implements Parcelable { throw new IllegalArgumentException("<pointer-icon> is missing bitmap attribute."); } - Drawable drawable = resources.getDrawable(bitmapRes); + Drawable drawable; + if (context == null) { + drawable = resources.getDrawable(bitmapRes); + } else { + drawable = context.getDrawable(bitmapRes); + } if (!(drawable instanceof BitmapDrawable)) { throw new IllegalArgumentException("<pointer-icon> bitmap attribute must " + "refer to a bitmap drawable."); diff --git a/core/java/android/view/RenderNode.java b/core/java/android/view/RenderNode.java new file mode 100644 index 0000000..30e4281 --- /dev/null +++ b/core/java/android/view/RenderNode.java @@ -0,0 +1,900 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.annotation.NonNull; +import android.graphics.Matrix; +import android.graphics.Outline; + +/** + * <p>A display list records a series of graphics related operations and can replay + * them later. Display lists are usually built by recording operations on a + * {@link HardwareCanvas}. Replaying the operations from a display list avoids + * executing application code on every frame, and is thus much more efficient.</p> + * + * <p>Display lists are used internally for all views by default, and are not + * typically used directly. One reason to consider using a display is a custom + * {@link View} implementation that needs to issue a large number of drawing commands. + * When the view invalidates, all the drawing commands must be reissued, even if + * large portions of the drawing command stream stay the same frame to frame, which + * can become a performance bottleneck. To solve this issue, a custom View might split + * its content into several display lists. A display list is updated only when its + * content, and only its content, needs to be updated.</p> + * + * <p>A text editor might for instance store each paragraph into its own display list. + * Thus when the user inserts or removes characters, only the display list of the + * affected paragraph needs to be recorded again.</p> + * + * <h3>Hardware acceleration</h3> + * <p>Display lists can only be replayed using a {@link HardwareCanvas}. They are not + * supported in software. Always make sure that the {@link android.graphics.Canvas} + * you are using to render a display list is hardware accelerated using + * {@link android.graphics.Canvas#isHardwareAccelerated()}.</p> + * + * <h3>Creating a display list</h3> + * <pre class="prettyprint"> + * HardwareRenderer renderer = myView.getHardwareRenderer(); + * if (renderer != null) { + * DisplayList displayList = renderer.createDisplayList(); + * HardwareCanvas canvas = displayList.start(width, height); + * try { + * // Draw onto the canvas + * // For instance: canvas.drawBitmap(...); + * } finally { + * displayList.end(); + * } + * } + * </pre> + * + * <h3>Rendering a display list on a View</h3> + * <pre class="prettyprint"> + * protected void onDraw(Canvas canvas) { + * if (canvas.isHardwareAccelerated()) { + * HardwareCanvas hardwareCanvas = (HardwareCanvas) canvas; + * hardwareCanvas.drawDisplayList(mDisplayList); + * } + * } + * </pre> + * + * <h3>Releasing resources</h3> + * <p>This step is not mandatory but recommended if you want to release resources + * held by a display list as soon as possible.</p> + * <pre class="prettyprint"> + * // Mark this display list invalid, it cannot be used for drawing anymore, + * // and release resources held by this display list + * displayList.clear(); + * </pre> + * + * <h3>Properties</h3> + * <p>In addition, a display list offers several properties, such as + * {@link #setScaleX(float)} or {@link #setLeft(int)}, that can be used to affect all + * the drawing commands recorded within. For instance, these properties can be used + * to move around a large number of images without re-issuing all the individual + * <code>drawBitmap()</code> calls.</p> + * + * <pre class="prettyprint"> + * private void createDisplayList() { + * mDisplayList = DisplayList.create("MyDisplayList"); + * HardwareCanvas canvas = mDisplayList.start(width, height); + * try { + * for (Bitmap b : mBitmaps) { + * canvas.drawBitmap(b, 0.0f, 0.0f, null); + * canvas.translate(0.0f, b.getHeight()); + * } + * } finally { + * displayList.end(); + * } + * } + * + * protected void onDraw(Canvas canvas) { + * if (canvas.isHardwareAccelerated()) { + * HardwareCanvas hardwareCanvas = (HardwareCanvas) canvas; + * hardwareCanvas.drawDisplayList(mDisplayList); + * } + * } + * + * private void moveContentBy(int x) { + * // This will move all the bitmaps recorded inside the display list + * // by x pixels to the right and redraw this view. All the commands + * // recorded in createDisplayList() won't be re-issued, only onDraw() + * // will be invoked and will execute very quickly + * mDisplayList.offsetLeftAndRight(x); + * invalidate(); + * } + * </pre> + * + * <h3>Threading</h3> + * <p>Display lists must be created on and manipulated from the UI thread only.</p> + * + * @hide + */ +public class RenderNode { + /** + * Flag used when calling + * {@link HardwareCanvas#drawDisplayList(RenderNode, android.graphics.Rect, int)} + * When this flag is set, draw operations lying outside of the bounds of the + * display list will be culled early. It is recommeneded to always set this + * flag. + * + * @hide + */ + public static final int FLAG_CLIP_CHILDREN = 0x1; + + // NOTE: The STATUS_* values *must* match the enum in DrawGlInfo.h + + /** + * Indicates that the display list is done drawing. + * + * @see HardwareCanvas#drawDisplayList(RenderNode, android.graphics.Rect, int) + * + * @hide + */ + public static final int STATUS_DONE = 0x0; + + /** + * Indicates that the display list needs another drawing pass. + * + * @see HardwareCanvas#drawDisplayList(RenderNode, android.graphics.Rect, int) + * + * @hide + */ + public static final int STATUS_DRAW = 0x1; + + /** + * Indicates that the display list needs to re-execute its GL functors. + * + * @see HardwareCanvas#drawDisplayList(RenderNode, android.graphics.Rect, int) + * @see HardwareCanvas#callDrawGLFunction(long) + * + * @hide + */ + public static final int STATUS_INVOKE = 0x2; + + /** + * Indicates that the display list performed GL drawing operations. + * + * @see HardwareCanvas#drawDisplayList(RenderNode, android.graphics.Rect, int) + * + * @hide + */ + public static final int STATUS_DREW = 0x4; + + private boolean mValid; + private final long mNativeRenderNode; + + private RenderNode(String name) { + mNativeRenderNode = nCreate(name); + } + + /** + * Creates a new display list that can be used to record batches of + * drawing operations. + * + * @param name The name of the display list, used for debugging purpose. May be null. + * + * @return A new display list. + * + * @hide + */ + public static RenderNode create(String name) { + return new RenderNode(name); + } + + /** + * Starts recording a display list for the render node. All + * operations performed on the returned canvas are recorded and + * stored in this display list. + * + * Calling this method will mark the render node invalid until + * {@link #end(HardwareCanvas)} is called. + * Only valid render nodes can be replayed. + * + * @param width The width of the recording viewport + * @param height The height of the recording viewport + * + * @return A canvas to record drawing operations. + * + * @see #end(HardwareCanvas) + * @see #isValid() + */ + public HardwareCanvas start(int width, int height) { + HardwareCanvas canvas = GLES20RecordingCanvas.obtain(); + canvas.setViewport(width, height); + // The dirty rect should always be null for a display list + canvas.onPreDraw(null); + return canvas; + } + + /** + * Ends the recording for this display list. A display list cannot be + * replayed if recording is not finished. Calling this method marks + * the display list valid and {@link #isValid()} will return true. + * + * @see #start(int, int) + * @see #isValid() + */ + public void end(HardwareCanvas endCanvas) { + if (!(endCanvas instanceof GLES20RecordingCanvas)) { + throw new IllegalArgumentException("Passed an invalid canvas to end!"); + } + + GLES20RecordingCanvas canvas = (GLES20RecordingCanvas) endCanvas; + canvas.onPostDraw(); + long renderNodeData = canvas.finishRecording(); + nSetDisplayListData(mNativeRenderNode, renderNodeData); + canvas.recycle(); + mValid = true; + } + + /** + * Reset native resources. This is called when cleaning up the state of display lists + * during destruction of hardware resources, to ensure that we do not hold onto + * obsolete resources after related resources are gone. + * + * @hide + */ + public void destroyDisplayListData() { + if (!mValid) return; + + nSetDisplayListData(mNativeRenderNode, 0); + mValid = false; + } + + /** + * Returns whether the RenderNode's display list content is currently usable. + * If this returns false, the display list should be re-recorded prior to replaying it. + * + * @return boolean true if the display list is able to be replayed, false otherwise. + */ + public boolean isValid() { return mValid; } + + long getNativeDisplayList() { + if (!mValid) { + throw new IllegalStateException("The display list is not valid."); + } + return mNativeRenderNode; + } + + /////////////////////////////////////////////////////////////////////////// + // Matrix manipulation + /////////////////////////////////////////////////////////////////////////// + + public boolean hasIdentityMatrix() { + return nHasIdentityMatrix(mNativeRenderNode); + } + + public void getMatrix(@NonNull Matrix outMatrix) { + nGetTransformMatrix(mNativeRenderNode, outMatrix.native_instance); + } + + public void getInverseMatrix(@NonNull Matrix outMatrix) { + nGetInverseTransformMatrix(mNativeRenderNode, outMatrix.native_instance); + } + + /////////////////////////////////////////////////////////////////////////// + // RenderProperty Setters + /////////////////////////////////////////////////////////////////////////// + + /** + * Set the caching property on the display list, which indicates whether the display list + * holds a layer. Layer display lists should avoid creating an alpha layer, since alpha is + * handled in the drawLayer operation directly (and more efficiently). + * + * @param caching true if the display list represents a hardware layer, false otherwise. + * + * @hide + */ + public void setCaching(boolean caching) { + nSetCaching(mNativeRenderNode, caching); + } + + /** + * Set whether the Render node should clip itself to its bounds. This property is controlled by + * the view's parent. + * + * @param clipToBounds true if the display list should clip to its bounds + */ + public void setClipToBounds(boolean clipToBounds) { + nSetClipToBounds(mNativeRenderNode, clipToBounds); + } + + /** + * Sets whether the display list should be drawn immediately after the + * closest ancestor display list containing a projection receiver. + * + * @param shouldProject true if the display list should be projected onto a + * containing volume. + */ + public void setProjectBackwards(boolean shouldProject) { + nSetProjectBackwards(mNativeRenderNode, shouldProject); + } + + /** + * Sets whether the display list is a projection receiver - that its parent + * DisplayList should draw any descendent DisplayLists with + * ProjectBackwards=true directly on top of it. Default value is false. + */ + public void setProjectionReceiver(boolean shouldRecieve) { + nSetProjectionReceiver(mNativeRenderNode, shouldRecieve); + } + + /** + * Sets the outline, defining the shape that casts a shadow, and the path to + * be clipped if setClipToOutline is set. + * + * Deep copies the data into native to simplify reference ownership. + */ + public void setOutline(Outline outline) { + if (outline == null) { + nSetOutlineEmpty(mNativeRenderNode); + } else if (!outline.isValid()) { + throw new IllegalArgumentException("Outline must be valid"); + } else if (outline.mRect != null) { + nSetOutlineRoundRect(mNativeRenderNode, outline.mRect.left, outline.mRect.top, + outline.mRect.right, outline.mRect.bottom, outline.mRadius); + } else if (outline.mPath != null) { + nSetOutlineConvexPath(mNativeRenderNode, outline.mPath.mNativePath); + } + } + + /** + * Enables or disables clipping to the outline. + * + * @param clipToOutline true if clipping to the outline. + */ + public void setClipToOutline(boolean clipToOutline) { + nSetClipToOutline(mNativeRenderNode, clipToOutline); + } + + /** + * Controls the RenderNode's circular reveal clip. + */ + public void setRevealClip(boolean shouldClip, boolean inverseClip, + float x, float y, float radius) { + nSetRevealClip(mNativeRenderNode, shouldClip, inverseClip, x, y, radius); + } + + /** + * Set the static matrix on the display list. The specified matrix is combined with other + * transforms (such as {@link #setScaleX(float)}, {@link #setRotation(float)}, etc.) + * + * @param matrix A transform matrix to apply to this display list + */ + public void setStaticMatrix(Matrix matrix) { + nSetStaticMatrix(mNativeRenderNode, matrix.native_instance); + } + + /** + * Set the Animation matrix on the display list. This matrix exists if an Animation is + * currently playing on a View, and is set on the display list during at draw() time. When + * the Animation finishes, the matrix should be cleared by sending <code>null</code> + * for the matrix parameter. + * + * @param matrix The matrix, null indicates that the matrix should be cleared. + * + * @hide + */ + public void setAnimationMatrix(Matrix matrix) { + nSetAnimationMatrix(mNativeRenderNode, + (matrix != null) ? matrix.native_instance : 0); + } + + /** + * Sets the translucency level for the display list. + * + * @param alpha The translucency of the display list, must be a value between 0.0f and 1.0f + * + * @see View#setAlpha(float) + * @see #getAlpha() + */ + public void setAlpha(float alpha) { + nSetAlpha(mNativeRenderNode, alpha); + } + + /** + * Returns the translucency level of this display list. + * + * @return A value between 0.0f and 1.0f + * + * @see #setAlpha(float) + */ + public float getAlpha() { + return nGetAlpha(mNativeRenderNode); + } + + /** + * Sets whether the display list renders content which overlaps. Non-overlapping rendering + * can use a fast path for alpha that avoids rendering to an offscreen buffer. By default + * display lists consider they do not have overlapping content. + * + * @param hasOverlappingRendering False if the content is guaranteed to be non-overlapping, + * true otherwise. + * + * @see android.view.View#hasOverlappingRendering() + * @see #hasOverlappingRendering() + */ + public void setHasOverlappingRendering(boolean hasOverlappingRendering) { + nSetHasOverlappingRendering(mNativeRenderNode, hasOverlappingRendering); + } + + /** + * Indicates whether the content of this display list overlaps. + * + * @return True if this display list renders content which overlaps, false otherwise. + * + * @see #setHasOverlappingRendering(boolean) + */ + public boolean hasOverlappingRendering() { + //noinspection SimplifiableIfStatement + return nHasOverlappingRendering(mNativeRenderNode); + } + + /** + * Sets the translation value for the display list on the X axis. + * + * @param translationX The X axis translation value of the display list, in pixels + * + * @see View#setTranslationX(float) + * @see #getTranslationX() + */ + public void setTranslationX(float translationX) { + nSetTranslationX(mNativeRenderNode, translationX); + } + + /** + * Returns the translation value for this display list on the X axis, in pixels. + * + * @see #setTranslationX(float) + */ + public float getTranslationX() { + return nGetTranslationX(mNativeRenderNode); + } + + /** + * Sets the translation value for the display list on the Y axis. + * + * @param translationY The Y axis translation value of the display list, in pixels + * + * @see View#setTranslationY(float) + * @see #getTranslationY() + */ + public void setTranslationY(float translationY) { + nSetTranslationY(mNativeRenderNode, translationY); + } + + /** + * Returns the translation value for this display list on the Y axis, in pixels. + * + * @see #setTranslationY(float) + */ + public float getTranslationY() { + return nGetTranslationY(mNativeRenderNode); + } + + /** + * Sets the translation value for the display list on the Z axis. + * + * @see View#setTranslationZ(float) + * @see #getTranslationZ() + */ + public void setTranslationZ(float translationZ) { + nSetTranslationZ(mNativeRenderNode, translationZ); + } + + /** + * Returns the translation value for this display list on the Z axis. + * + * @see #setTranslationZ(float) + */ + public float getTranslationZ() { + return nGetTranslationZ(mNativeRenderNode); + } + + /** + * Sets the rotation value for the display list around the Z axis. + * + * @param rotation The rotation value of the display list, in degrees + * + * @see View#setRotation(float) + * @see #getRotation() + */ + public void setRotation(float rotation) { + nSetRotation(mNativeRenderNode, rotation); + } + + /** + * Returns the rotation value for this display list around the Z axis, in degrees. + * + * @see #setRotation(float) + */ + public float getRotation() { + return nGetRotation(mNativeRenderNode); + } + + /** + * Sets the rotation value for the display list around the X axis. + * + * @param rotationX The rotation value of the display list, in degrees + * + * @see View#setRotationX(float) + * @see #getRotationX() + */ + public void setRotationX(float rotationX) { + nSetRotationX(mNativeRenderNode, rotationX); + } + + /** + * Returns the rotation value for this display list around the X axis, in degrees. + * + * @see #setRotationX(float) + */ + public float getRotationX() { + return nGetRotationX(mNativeRenderNode); + } + + /** + * Sets the rotation value for the display list around the Y axis. + * + * @param rotationY The rotation value of the display list, in degrees + * + * @see View#setRotationY(float) + * @see #getRotationY() + */ + public void setRotationY(float rotationY) { + nSetRotationY(mNativeRenderNode, rotationY); + } + + /** + * Returns the rotation value for this display list around the Y axis, in degrees. + * + * @see #setRotationY(float) + */ + public float getRotationY() { + return nGetRotationY(mNativeRenderNode); + } + + /** + * Sets the scale value for the display list on the X axis. + * + * @param scaleX The scale value of the display list + * + * @see View#setScaleX(float) + * @see #getScaleX() + */ + public void setScaleX(float scaleX) { + nSetScaleX(mNativeRenderNode, scaleX); + } + + /** + * Returns the scale value for this display list on the X axis. + * + * @see #setScaleX(float) + */ + public float getScaleX() { + return nGetScaleX(mNativeRenderNode); + } + + /** + * Sets the scale value for the display list on the Y axis. + * + * @param scaleY The scale value of the display list + * + * @see View#setScaleY(float) + * @see #getScaleY() + */ + public void setScaleY(float scaleY) { + nSetScaleY(mNativeRenderNode, scaleY); + } + + /** + * Returns the scale value for this display list on the Y axis. + * + * @see #setScaleY(float) + */ + public float getScaleY() { + return nGetScaleY(mNativeRenderNode); + } + + /** + * Sets the pivot value for the display list on the X axis + * + * @param pivotX The pivot value of the display list on the X axis, in pixels + * + * @see View#setPivotX(float) + * @see #getPivotX() + */ + public void setPivotX(float pivotX) { + nSetPivotX(mNativeRenderNode, pivotX); + } + + /** + * Returns the pivot value for this display list on the X axis, in pixels. + * + * @see #setPivotX(float) + */ + public float getPivotX() { + return nGetPivotX(mNativeRenderNode); + } + + /** + * Sets the pivot value for the display list on the Y axis + * + * @param pivotY The pivot value of the display list on the Y axis, in pixels + * + * @see View#setPivotY(float) + * @see #getPivotY() + */ + public void setPivotY(float pivotY) { + nSetPivotY(mNativeRenderNode, pivotY); + } + + /** + * Returns the pivot value for this display list on the Y axis, in pixels. + * + * @see #setPivotY(float) + */ + public float getPivotY() { + return nGetPivotY(mNativeRenderNode); + } + + public boolean isPivotExplicitlySet() { + return nIsPivotExplicitlySet(mNativeRenderNode); + } + + /** + * Sets the camera distance for the display list. Refer to + * {@link View#setCameraDistance(float)} for more information on how to + * use this property. + * + * @param distance The distance in Z of the camera of the display list + * + * @see View#setCameraDistance(float) + * @see #getCameraDistance() + */ + public void setCameraDistance(float distance) { + nSetCameraDistance(mNativeRenderNode, distance); + } + + /** + * Returns the distance in Z of the camera of the display list. + * + * @see #setCameraDistance(float) + */ + public float getCameraDistance() { + return nGetCameraDistance(mNativeRenderNode); + } + + /** + * Sets the left position for the display list. + * + * @param left The left position, in pixels, of the display list + * + * @see View#setLeft(int) + * @see #getLeft() + */ + public void setLeft(int left) { + nSetLeft(mNativeRenderNode, left); + } + + /** + * Returns the left position for the display list in pixels. + * + * @see #setLeft(int) + */ + public float getLeft() { + return nGetLeft(mNativeRenderNode); + } + + /** + * Sets the top position for the display list. + * + * @param top The top position, in pixels, of the display list + * + * @see View#setTop(int) + * @see #getTop() + */ + public void setTop(int top) { + nSetTop(mNativeRenderNode, top); + } + + /** + * Returns the top position for the display list in pixels. + * + * @see #setTop(int) + */ + public float getTop() { + return nGetTop(mNativeRenderNode); + } + + /** + * Sets the right position for the display list. + * + * @param right The right position, in pixels, of the display list + * + * @see View#setRight(int) + * @see #getRight() + */ + public void setRight(int right) { + nSetRight(mNativeRenderNode, right); + } + + /** + * Returns the right position for the display list in pixels. + * + * @see #setRight(int) + */ + public float getRight() { + return nGetRight(mNativeRenderNode); + } + + /** + * Sets the bottom position for the display list. + * + * @param bottom The bottom position, in pixels, of the display list + * + * @see View#setBottom(int) + * @see #getBottom() + */ + public void setBottom(int bottom) { + nSetBottom(mNativeRenderNode, bottom); + } + + /** + * Returns the bottom position for the display list in pixels. + * + * @see #setBottom(int) + */ + public float getBottom() { + return nGetBottom(mNativeRenderNode); + } + + /** + * Sets the left and top positions for the display list + * + * @param left The left position of the display list, in pixels + * @param top The top position of the display list, in pixels + * @param right The right position of the display list, in pixels + * @param bottom The bottom position of the display list, in pixels + * + * @see View#setLeft(int) + * @see View#setTop(int) + * @see View#setRight(int) + * @see View#setBottom(int) + */ + public void setLeftTopRightBottom(int left, int top, int right, int bottom) { + nSetLeftTopRightBottom(mNativeRenderNode, left, top, right, bottom); + } + + /** + * Offsets the left and right positions for the display list + * + * @param offset The amount that the left and right positions of the display + * list are offset, in pixels + * + * @see View#offsetLeftAndRight(int) + */ + public void offsetLeftAndRight(float offset) { + nOffsetLeftAndRight(mNativeRenderNode, offset); + } + + /** + * Offsets the top and bottom values for the display list + * + * @param offset The amount that the top and bottom positions of the display + * list are offset, in pixels + * + * @see View#offsetTopAndBottom(int) + */ + public void offsetTopAndBottom(float offset) { + nOffsetTopAndBottom(mNativeRenderNode, offset); + } + + /** + * Outputs the display list to the log. This method exists for use by + * tools to output display lists for selected nodes to the log. + * + * @hide + */ + public void output() { + nOutput(mNativeRenderNode); + } + + /////////////////////////////////////////////////////////////////////////// + // Native methods + /////////////////////////////////////////////////////////////////////////// + + private static native long nCreate(String name); + private static native void nDestroyRenderNode(long renderNode); + private static native void nSetDisplayListData(long renderNode, long newData); + + // Matrix + + private static native void nGetTransformMatrix(long renderNode, long nativeMatrix); + private static native void nGetInverseTransformMatrix(long renderNode, long nativeMatrix); + private static native boolean nHasIdentityMatrix(long renderNode); + + // Properties + + private static native void nOffsetTopAndBottom(long renderNode, float offset); + private static native void nOffsetLeftAndRight(long renderNode, float offset); + private static native void nSetLeftTopRightBottom(long renderNode, int left, int top, + int right, int bottom); + private static native void nSetBottom(long renderNode, int bottom); + private static native void nSetRight(long renderNode, int right); + private static native void nSetTop(long renderNode, int top); + private static native void nSetLeft(long renderNode, int left); + private static native void nSetCameraDistance(long renderNode, float distance); + private static native void nSetPivotY(long renderNode, float pivotY); + private static native void nSetPivotX(long renderNode, float pivotX); + private static native void nSetCaching(long renderNode, boolean caching); + private static native void nSetClipToBounds(long renderNode, boolean clipToBounds); + private static native void nSetProjectBackwards(long renderNode, boolean shouldProject); + private static native void nSetProjectionReceiver(long renderNode, boolean shouldRecieve); + private static native void nSetOutlineRoundRect(long renderNode, int left, int top, + int right, int bottom, float radius); + private static native void nSetOutlineConvexPath(long renderNode, long nativePath); + private static native void nSetOutlineEmpty(long renderNode); + private static native void nSetClipToOutline(long renderNode, boolean clipToOutline); + private static native void nSetRevealClip(long renderNode, + boolean shouldClip, boolean inverseClip, float x, float y, float radius); + private static native void nSetAlpha(long renderNode, float alpha); + private static native void nSetHasOverlappingRendering(long renderNode, + boolean hasOverlappingRendering); + private static native void nSetTranslationX(long renderNode, float translationX); + private static native void nSetTranslationY(long renderNode, float translationY); + private static native void nSetTranslationZ(long renderNode, float translationZ); + private static native void nSetRotation(long renderNode, float rotation); + private static native void nSetRotationX(long renderNode, float rotationX); + private static native void nSetRotationY(long renderNode, float rotationY); + private static native void nSetScaleX(long renderNode, float scaleX); + private static native void nSetScaleY(long renderNode, float scaleY); + private static native void nSetStaticMatrix(long renderNode, long nativeMatrix); + private static native void nSetAnimationMatrix(long renderNode, long animationMatrix); + + private static native boolean nHasOverlappingRendering(long renderNode); + private static native float nGetAlpha(long renderNode); + private static native float nGetLeft(long renderNode); + private static native float nGetTop(long renderNode); + private static native float nGetRight(long renderNode); + private static native float nGetBottom(long renderNode); + private static native float nGetCameraDistance(long renderNode); + private static native float nGetScaleX(long renderNode); + private static native float nGetScaleY(long renderNode); + private static native float nGetTranslationX(long renderNode); + private static native float nGetTranslationY(long renderNode); + private static native float nGetTranslationZ(long renderNode); + private static native float nGetRotation(long renderNode); + private static native float nGetRotationX(long renderNode); + private static native float nGetRotationY(long renderNode); + private static native boolean nIsPivotExplicitlySet(long renderNode); + private static native float nGetPivotX(long renderNode); + private static native float nGetPivotY(long renderNode); + private static native void nOutput(long renderNode); + + /////////////////////////////////////////////////////////////////////////// + // Finalization + /////////////////////////////////////////////////////////////////////////// + + @Override + protected void finalize() throws Throwable { + try { + nDestroyRenderNode(mNativeRenderNode); + } finally { + super.finalize(); + } + } +} diff --git a/core/java/android/view/Surface.java b/core/java/android/view/Surface.java index 91645e7..fdaae01 100644 --- a/core/java/android/view/Surface.java +++ b/core/java/android/view/Surface.java @@ -16,6 +16,7 @@ package android.view; +import android.annotation.IntDef; import android.content.res.CompatibilityInfo.Translator; import android.graphics.Canvas; import android.graphics.Matrix; @@ -24,6 +25,10 @@ import android.graphics.SurfaceTexture; import android.os.Parcel; import android.os.Parcelable; import android.util.Log; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + import dalvik.system.CloseGuard; /** @@ -80,6 +85,11 @@ public class Surface implements Parcelable { // non compatibility mode. private Matrix mCompatibleMatrix; + /** @hide */ + @IntDef({ROTATION_0, ROTATION_90, ROTATION_180, ROTATION_270}) + @Retention(RetentionPolicy.SOURCE) + public @interface Rotation {} + /** * Rotation constant: 0 degree rotation (natural orientation) */ diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java index eea5884..2d55a01 100644 --- a/core/java/android/view/SurfaceControl.java +++ b/core/java/android/view/SurfaceControl.java @@ -20,9 +20,7 @@ import dalvik.system.CloseGuard; import android.graphics.Bitmap; import android.graphics.Rect; import android.graphics.Region; -import android.view.Surface; import android.os.IBinder; -import android.os.SystemProperties; import android.util.Log; import android.view.Surface.OutOfResourcesException; @@ -40,9 +38,11 @@ public class SurfaceControl { private static native void nativeDestroy(long nativeObject); private static native Bitmap nativeScreenshot(IBinder displayToken, - int width, int height, int minLayer, int maxLayer, boolean allLayers); + int width, int height, int minLayer, int maxLayer, boolean allLayers, + boolean useIdentityTransform); private static native void nativeScreenshot(IBinder displayToken, Surface consumer, - int width, int height, int minLayer, int maxLayer, boolean allLayers); + int width, int height, int minLayer, int maxLayer, boolean allLayers, + boolean useIdentityTransform); private static native void nativeOpenTransaction(); private static native void nativeCloseTransaction(); @@ -58,6 +58,11 @@ public class SurfaceControl { private static native void nativeSetWindowCrop(long nativeObject, int l, int t, int r, int b); private static native void nativeSetLayerStack(long nativeObject, int layerStack); + private static native boolean nativeClearContentFrameStats(long nativeObject); + private static native boolean nativeGetContentFrameStats(long nativeObject, WindowContentFrameStats outStats); + private static native boolean nativeClearAnimationFrameStats(); + private static native boolean nativeGetAnimationFrameStats(WindowAnimationFrameStats outStats); + private static native IBinder nativeGetBuiltInDisplay(int physicalDisplayId); private static native IBinder nativeCreateDisplay(String name, boolean secure); private static native void nativeDestroyDisplay(IBinder displayToken); @@ -178,13 +183,13 @@ public class SurfaceControl { * Equivalent to calling hide(). * Updates the value set during Surface creation (see {@link #HIDDEN}). */ - public static final int SURFACE_HIDDEN = 0x01; + private static final int SURFACE_HIDDEN = 0x01; /** * Surface flag: composite without blending when possible. * Updates the value set during Surface creation (see {@link #OPAQUE}). */ - public static final int SURFACE_OPAQUE = 0x02; + private static final int SURFACE_OPAQUE = 0x02; /* built-in physical display ids (keep in sync with ISurfaceComposer.h) @@ -192,13 +197,13 @@ public class SurfaceControl { /** * Built-in physical display id: Main display. - * Use only with {@link SurfaceControl#getBuiltInDisplay()}. + * Use only with {@link SurfaceControl#getBuiltInDisplay(int)}. */ public static final int BUILT_IN_DISPLAY_ID_MAIN = 0; /** * Built-in physical display id: Attached HDMI display. - * Use only with {@link SurfaceControl#getBuiltInDisplay()}. + * Use only with {@link SurfaceControl#getBuiltInDisplay(int)}. */ public static final int BUILT_IN_DISPLAY_ID_HDMI = 1; @@ -356,6 +361,24 @@ public class SurfaceControl { nativeSetTransparentRegionHint(mNativeObject, region); } + public boolean clearContentFrameStats() { + checkNotReleased(); + return nativeClearContentFrameStats(mNativeObject); + } + + public boolean getContentFrameStats(WindowContentFrameStats outStats) { + checkNotReleased(); + return nativeGetContentFrameStats(mNativeObject, outStats); + } + + public static boolean clearAnimationFrameStats() { + return nativeClearAnimationFrameStats(); + } + + public static boolean getAnimationFrameStats(WindowAnimationFrameStats outStats) { + return nativeGetAnimationFrameStats(outStats); + } + /** * Sets an alpha value for the entire Surface. This value is combined with the * per-pixel alpha. It may be used with opaque Surfaces. @@ -370,18 +393,6 @@ public class SurfaceControl { nativeSetMatrix(mNativeObject, dsdx, dtdx, dsdy, dtdy); } - /** - * Sets and clears flags, such as {@link #SURFACE_HIDDEN}. The new value will be: - * <p> - * <code>newFlags = (oldFlags & ~mask) | (flags & mask)</code> - * <p> - * Note this does not take the same set of flags as the constructor. - */ - public void setFlags(int flags, int mask) { - checkNotReleased(); - nativeSetFlags(mNativeObject, flags, mask); - } - public void setWindowCrop(Rect crop) { checkNotReleased(); if (crop != null) { @@ -553,7 +564,6 @@ public class SurfaceControl { return nativeGetBuiltInDisplay(builtInDisplayId); } - /** * Copy the current screen contents into the provided {@link Surface} * @@ -567,10 +577,15 @@ public class SurfaceControl { * include in the screenshot. * @param maxLayer The highest (top-most Z order) surface layer to * include in the screenshot. + * @param useIdentityTransform Replace whatever transformation (rotation, + * scaling, translation) the surface layers are currently using with the + * identity transformation while taking the screenshot. */ public static void screenshot(IBinder display, Surface consumer, - int width, int height, int minLayer, int maxLayer) { - screenshot(display, consumer, width, height, minLayer, maxLayer, false); + int width, int height, int minLayer, int maxLayer, + boolean useIdentityTransform) { + screenshot(display, consumer, width, height, minLayer, maxLayer, false, + useIdentityTransform); } /** @@ -585,7 +600,7 @@ public class SurfaceControl { */ public static void screenshot(IBinder display, Surface consumer, int width, int height) { - screenshot(display, consumer, width, height, 0, 0, true); + screenshot(display, consumer, width, height, 0, 0, true, false); } /** @@ -595,10 +610,9 @@ public class SurfaceControl { * @param consumer The {@link Surface} to take the screenshot into. */ public static void screenshot(IBinder display, Surface consumer) { - screenshot(display, consumer, 0, 0, 0, 0, true); + screenshot(display, consumer, 0, 0, 0, 0, true, false); } - /** * Copy the current screen contents into a bitmap and return it. * @@ -615,20 +629,25 @@ public class SurfaceControl { * include in the screenshot. * @param maxLayer The highest (top-most Z order) surface layer to * include in the screenshot. + * @param useIdentityTransform Replace whatever transformation (rotation, + * scaling, translation) the surface layers are currently using with the + * identity transformation while taking the screenshot. * @return Returns a Bitmap containing the screen contents, or null * if an error occurs. Make sure to call Bitmap.recycle() as soon as * possible, once its content is not needed anymore. */ - public static Bitmap screenshot(int width, int height, int minLayer, int maxLayer) { + public static Bitmap screenshot(int width, int height, int minLayer, int maxLayer, + boolean useIdentityTransform) { // TODO: should take the display as a parameter IBinder displayToken = SurfaceControl.getBuiltInDisplay( SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN); - return nativeScreenshot(displayToken, width, height, minLayer, maxLayer, false); + return nativeScreenshot(displayToken, width, height, minLayer, maxLayer, false, + useIdentityTransform); } /** - * Like {@link SurfaceControl#screenshot(int, int, int, int)} but includes all - * Surfaces in the screenshot. + * Like {@link SurfaceControl#screenshot(int, int, int, int, boolean)} but + * includes all Surfaces in the screenshot. * * @param width The desired width of the returned bitmap; the raw * screen will be scaled down to this size. @@ -642,17 +661,19 @@ public class SurfaceControl { // TODO: should take the display as a parameter IBinder displayToken = SurfaceControl.getBuiltInDisplay( SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN); - return nativeScreenshot(displayToken, width, height, 0, 0, true); + return nativeScreenshot(displayToken, width, height, 0, 0, true, false); } private static void screenshot(IBinder display, Surface consumer, - int width, int height, int minLayer, int maxLayer, boolean allLayers) { + int width, int height, int minLayer, int maxLayer, boolean allLayers, + boolean useIdentityTransform) { if (display == null) { throw new IllegalArgumentException("displayToken must not be null"); } if (consumer == null) { throw new IllegalArgumentException("consumer must not be null"); } - nativeScreenshot(display, consumer, width, height, minLayer, maxLayer, allLayers); + nativeScreenshot(display, consumer, width, height, minLayer, maxLayer, allLayers, + useIdentityTransform); } } diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java index 22d4c9b..23123dd 100644 --- a/core/java/android/view/SurfaceView.java +++ b/core/java/android/view/SurfaceView.java @@ -188,8 +188,13 @@ public class SurfaceView extends View { init(); } - public SurfaceView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public SurfaceView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + public SurfaceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); init(); } @@ -250,8 +255,9 @@ public class SurfaceView extends View { updateWindow(false, false); } + /** @hide */ @Override - protected void onDetachedFromWindow() { + protected void onDetachedFromWindowInternal() { if (mGlobalListenersAdded) { ViewTreeObserver observer = getViewTreeObserver(); observer.removeOnScrollChangedListener(mScrollChangedListener); @@ -273,7 +279,7 @@ public class SurfaceView extends View { mSession = null; mLayout.token = null; - super.onDetachedFromWindow(); + super.onDetachedFromWindowInternal(); } @Override diff --git a/core/java/android/view/TextureView.java b/core/java/android/view/TextureView.java index b78af2e..3cfe5e9 100644 --- a/core/java/android/view/TextureView.java +++ b/core/java/android/view/TextureView.java @@ -156,14 +156,32 @@ public class TextureView extends View { * * @param context The context to associate this view with. * @param attrs The attributes of the XML tag that is inflating the view. - * @param defStyle The default style to apply to this view. If 0, no style - * will be applied (beyond what is included in the theme). This may - * either be an attribute resource, whose value will be retrieved - * from the current theme, or an explicit style resource. + * @param defStyleAttr An attribute in the current theme that contains a + * reference to a style resource that supplies default values for + * the view. Can be 0 to not look for defaults. */ @SuppressWarnings({"UnusedDeclaration"}) - public TextureView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public TextureView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + /** + * Creates a new TextureView. + * + * @param context The context to associate this view with. + * @param attrs The attributes of the XML tag that is inflating the view. + * @param defStyleAttr An attribute in the current theme that contains a + * reference to a style resource that supplies default values for + * the view. Can be 0 to not look for defaults. + * @param defStyleRes A resource identifier of a style resource that + * supplies default values for the view, used only if + * defStyleAttr is 0 or can not be found in the theme. Can be 0 + * to not look for defaults. + */ + @SuppressWarnings({"UnusedDeclaration"}) + public TextureView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); init(); } @@ -210,29 +228,16 @@ public class TextureView extends View { } } + /** @hide */ @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - if (mLayer != null) { - boolean success = executeHardwareAction(new Runnable() { - @Override - public void run() { - destroySurface(); - } - }); - - if (!success) { - Log.w(LOG_TAG, "TextureView was not able to destroy its surface: " + this); - } - } + protected void onDetachedFromWindowInternal() { + destroySurface(); + super.onDetachedFromWindowInternal(); } private void destroySurface() { if (mLayer != null) { - mSurface.detachFromGLContext(); - // SurfaceTexture owns the texture name and detachFromGLContext - // should have deleted it - mLayer.clearStorage(); + mLayer.detachSurfaceTexture(mSurface); boolean shouldRelease = true; if (mListener != null) { @@ -357,7 +362,7 @@ public class TextureView extends View { return null; } - mLayer = mAttachInfo.mHardwareRenderer.createHardwareLayer(mOpaque); + mLayer = mAttachInfo.mHardwareRenderer.createTextureLayer(); if (!mUpdateSurface) { // Create a new SurfaceTexture for the layer. mSurface = mAttachInfo.mHardwareRenderer.createSurfaceTexture(mLayer); @@ -398,7 +403,7 @@ public class TextureView extends View { updateLayer(); mMatrixChanged = true; - mAttachInfo.mHardwareRenderer.setSurfaceTexture(mLayer, mSurface); + mLayer.setSurfaceTexture(mSurface); mSurface.setDefaultBufferSize(getWidth(), getHeight()); } @@ -451,7 +456,8 @@ public class TextureView extends View { } } - mLayer.update(getWidth(), getHeight(), mOpaque); + mLayer.prepare(getWidth(), getHeight(), mOpaque); + mLayer.updateSurfaceTexture(); if (mListener != null) { mListener.onSurfaceTextureUpdated(mSurface); @@ -589,14 +595,6 @@ public class TextureView extends View { */ public Bitmap getBitmap(Bitmap bitmap) { if (bitmap != null && isAvailable()) { - AttachInfo info = mAttachInfo; - if (info != null && info.mHardwareRenderer != null && - info.mHardwareRenderer.isEnabled()) { - if (!info.mHardwareRenderer.validate()) { - throw new IllegalStateException("Could not acquire hardware rendering context"); - } - } - applyUpdate(); applyTransformMatrix(); diff --git a/core/java/android/view/ThreadedRenderer.java b/core/java/android/view/ThreadedRenderer.java new file mode 100644 index 0000000..1429837 --- /dev/null +++ b/core/java/android/view/ThreadedRenderer.java @@ -0,0 +1,313 @@ +/* + * 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.view; + +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.graphics.SurfaceTexture; +import android.os.SystemClock; +import android.os.Trace; +import android.view.Surface.OutOfResourcesException; +import android.view.View.AttachInfo; + +import java.io.PrintWriter; + +/** + * Hardware renderer that proxies the rendering to a render thread. Most calls + * are currently synchronous. + * TODO: Make draw() async. + * TODO: Figure out how to share the DisplayList between two threads (global lock?) + * + * The UI thread can block on the RenderThread, but RenderThread must never + * block on the UI thread. + * + * ThreadedRenderer creates an instance of RenderProxy. RenderProxy in turn creates + * and manages a CanvasContext on the RenderThread. The CanvasContext is fully managed + * by the lifecycle of the RenderProxy. + * + * Note that although currently the EGL context & surfaces are created & managed + * by the render thread, the goal is to move that into a shared structure that can + * be managed by both threads. EGLSurface creation & deletion should ideally be + * done on the UI thread and not the RenderThread to avoid stalling the + * RenderThread with surface buffer allocation. + * + * @hide + */ +public class ThreadedRenderer extends HardwareRenderer { + private static final String LOGTAG = "ThreadedRenderer"; + + private static final Rect NULL_RECT = new Rect(-1, -1, -1, -1); + + private int mWidth, mHeight; + private long mNativeProxy; + private boolean mInitialized = false; + private RenderNode mRootNode; + + ThreadedRenderer(boolean translucent) { + mNativeProxy = nCreateProxy(translucent); + mRootNode = RenderNode.create("RootNode"); + mRootNode.setClipToBounds(false); + } + + @Override + void destroy(boolean full) { + mInitialized = false; + updateEnabledState(null); + nDestroyCanvasAndSurface(mNativeProxy); + } + + private void updateEnabledState(Surface surface) { + if (surface == null || !surface.isValid()) { + setEnabled(false); + } else { + setEnabled(mInitialized); + } + } + + @Override + boolean initialize(Surface surface) throws OutOfResourcesException { + mInitialized = true; + updateEnabledState(surface); + return nInitialize(mNativeProxy, surface); + } + + @Override + void updateSurface(Surface surface) throws OutOfResourcesException { + updateEnabledState(surface); + nUpdateSurface(mNativeProxy, surface); + } + + @Override + void pauseSurface(Surface surface) { + nPauseSurface(mNativeProxy, surface); + } + + @Override + void destroyHardwareResources(View view) { + destroyResources(view); + // TODO: GLES20Canvas.flushCaches(GLES20Canvas.FLUSH_CACHES_LAYERS); + } + + private static void destroyResources(View view) { + view.destroyHardwareResources(); + + if (view instanceof ViewGroup) { + ViewGroup group = (ViewGroup) view; + + int count = group.getChildCount(); + for (int i = 0; i < count; i++) { + destroyResources(group.getChildAt(i)); + } + } + } + + @Override + void invalidate(Surface surface) { + updateSurface(surface); + } + + @Override + boolean safelyRun(Runnable action) { + nRunWithGlContext(mNativeProxy, action); + return true; + } + + @Override + void setup(int width, int height) { + mWidth = width; + mHeight = height; + mRootNode.setLeftTopRightBottom(0, 0, mWidth, mHeight); + nSetup(mNativeProxy, width, height); + } + + @Override + int getWidth() { + return mWidth; + } + + @Override + int getHeight() { + return mHeight; + } + + @Override + void dumpGfxInfo(PrintWriter pw) { + // TODO Auto-generated method stub + } + + @Override + long getFrameCount() { + // TODO Auto-generated method stub + return 0; + } + + @Override + boolean loadSystemProperties() { + return false; + } + + /** + * TODO: Remove + * Temporary hack to allow RenderThreadTest prototype app to trigger + * replaying a DisplayList after modifying the displaylist properties + * + * @hide */ + public void repeatLastDraw() { + } + + private void updateRootDisplayList(View view, HardwareDrawCallbacks callbacks) { + view.mPrivateFlags |= View.PFLAG_DRAWN; + + view.mRecreateDisplayList = (view.mPrivateFlags & View.PFLAG_INVALIDATED) + == View.PFLAG_INVALIDATED; + view.mPrivateFlags &= ~View.PFLAG_INVALIDATED; + + Trace.traceBegin(Trace.TRACE_TAG_VIEW, "getDisplayList"); + HardwareCanvas canvas = mRootNode.start(mWidth, mHeight); + callbacks.onHardwarePostDraw(canvas); + canvas.drawDisplayList(view.getDisplayList()); + callbacks.onHardwarePostDraw(canvas); + mRootNode.end(canvas); + Trace.traceEnd(Trace.TRACE_TAG_VIEW); + + view.mRecreateDisplayList = false; + } + + @Override + void draw(View view, AttachInfo attachInfo, HardwareDrawCallbacks callbacks, Rect dirty) { + attachInfo.mIgnoreDirtyState = true; + attachInfo.mDrawingTime = SystemClock.uptimeMillis(); + + updateRootDisplayList(view, callbacks); + + if (dirty == null) { + dirty = NULL_RECT; + } + nDrawDisplayList(mNativeProxy, mRootNode.getNativeDisplayList(), + dirty.left, dirty.top, dirty.right, dirty.bottom); + } + + @Override + void detachFunctor(long functor) { + // no-op, we never attach functors to need to detach them + } + + @Override + void attachFunctor(AttachInfo attachInfo, long functor) { + invokeFunctor(functor, true); + } + + @Override + public void invokeFunctor(long functor, boolean waitForCompletion) { + nInvokeFunctor(mNativeProxy, functor, waitForCompletion); + } + + @Override + HardwareLayer createDisplayListLayer(int width, int height) { + long layer = nCreateDisplayListLayer(mNativeProxy, width, height); + return HardwareLayer.adoptDisplayListLayer(this, layer); + } + + @Override + HardwareLayer createTextureLayer() { + long layer = nCreateTextureLayer(mNativeProxy); + return HardwareLayer.adoptTextureLayer(this, layer); + } + + @Override + SurfaceTexture createSurfaceTexture(final HardwareLayer layer) { + final SurfaceTexture[] ret = new SurfaceTexture[1]; + nRunWithGlContext(mNativeProxy, new Runnable() { + @Override + public void run() { + ret[0] = layer.createSurfaceTexture(); + } + }); + return ret[0]; + } + + @Override + boolean copyLayerInto(final HardwareLayer layer, final Bitmap bitmap) { + return nCopyLayerInto(mNativeProxy, + layer.getDeferredLayerUpdater(), bitmap.mNativeBitmap); + } + + @Override + void pushLayerUpdate(HardwareLayer layer) { + // TODO: Remove this, it's not needed outside of GLRenderer + } + + @Override + void onLayerCreated(HardwareLayer layer) { + // TODO: Is this actually useful? + } + + @Override + void flushLayerUpdates() { + // TODO: Figure out what this should do or remove it + } + + @Override + void onLayerDestroyed(HardwareLayer layer) { + nDestroyLayer(mNativeProxy, layer.getDeferredLayerUpdater()); + } + + @Override + void setName(String name) { + } + + @Override + void fence() { + nFence(mNativeProxy); + } + + @Override + protected void finalize() throws Throwable { + try { + nDeleteProxy(mNativeProxy); + mNativeProxy = 0; + } finally { + super.finalize(); + } + } + + /** @hide */ + public static native void postToRenderThread(Runnable runnable); + + private static native long nCreateProxy(boolean translucent); + private static native void nDeleteProxy(long nativeProxy); + + private static native boolean nInitialize(long nativeProxy, Surface window); + private static native void nUpdateSurface(long nativeProxy, Surface window); + private static native void nPauseSurface(long nativeProxy, Surface window); + private static native void nSetup(long nativeProxy, int width, int height); + private static native void nSetDisplayListData(long nativeProxy, long displayList, + long newData); + private static native void nDrawDisplayList(long nativeProxy, long displayList, + int dirtyLeft, int dirtyTop, int dirtyRight, int dirtyBottom); + private static native void nRunWithGlContext(long nativeProxy, Runnable runnable); + private static native void nDestroyCanvasAndSurface(long nativeProxy); + + private static native void nInvokeFunctor(long nativeProxy, long functor, boolean waitForCompletion); + + private static native long nCreateDisplayListLayer(long nativeProxy, int width, int height); + private static native long nCreateTextureLayer(long nativeProxy); + private static native boolean nCopyLayerInto(long nativeProxy, long layer, long bitmap); + private static native void nDestroyLayer(long nativeProxy, long layer); + + private static native void nFence(long nativeProxy); +} diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index eb7f45e..f44cc87 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -16,6 +16,11 @@ package android.view; +import android.animation.RevealAnimator; +import android.animation.ValueAnimator; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.ClipData; import android.content.Context; import android.content.res.Configuration; @@ -28,6 +33,7 @@ import android.graphics.Insets; import android.graphics.Interpolator; import android.graphics.LinearGradient; import android.graphics.Matrix; +import android.graphics.Outline; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.Point; @@ -86,6 +92,8 @@ import com.android.internal.view.menu.MenuBuilder; import com.google.android.collect.Lists; import com.google.android.collect.Maps; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; @@ -95,7 +103,9 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicInteger; @@ -658,6 +668,7 @@ import java.util.concurrent.atomic.AtomicInteger; * @attr ref android.R.styleable#View_scrollbarTrackVertical * @attr ref android.R.styleable#View_scrollbarAlwaysDrawHorizontalTrack * @attr ref android.R.styleable#View_scrollbarAlwaysDrawVerticalTrack + * @attr ref android.R.styleable#View_sharedElementName * @attr ref android.R.styleable#View_soundEffectsEnabled * @attr ref android.R.styleable#View_tag * @attr ref android.R.styleable#View_textAlignment @@ -666,6 +677,7 @@ import java.util.concurrent.atomic.AtomicInteger; * @attr ref android.R.styleable#View_transformPivotY * @attr ref android.R.styleable#View_translationX * @attr ref android.R.styleable#View_translationY + * @attr ref android.R.styleable#View_translationZ * @attr ref android.R.styleable#View_visibility * * @see android.view.ViewGroup @@ -729,6 +741,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ private static final int FITS_SYSTEM_WINDOWS = 0x00000002; + /** @hide */ + @IntDef({VISIBLE, INVISIBLE, GONE}) + @Retention(RetentionPolicy.SOURCE) + public @interface Visibility {} + /** * This view is visible. * Use with {@link #setVisibility} and <a href="#attr_android:visibility">{@code @@ -896,6 +913,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ static final int FOCUSABLE_IN_TOUCH_MODE = 0x00040000; + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({DRAWING_CACHE_QUALITY_LOW, DRAWING_CACHE_QUALITY_HIGH, DRAWING_CACHE_QUALITY_AUTO}) + public @interface DrawingCacheQuality {} + /** * <p>Enables low quality mode for the drawing cache.</p> */ @@ -940,6 +962,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ static final int DUPLICATE_PARENT_STATE = 0x00400000; + /** @hide */ + @IntDef({ + SCROLLBARS_INSIDE_OVERLAY, + SCROLLBARS_INSIDE_INSET, + SCROLLBARS_OUTSIDE_OVERLAY, + SCROLLBARS_OUTSIDE_INSET + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ScrollBarStyle {} + /** * The scrollbar style to display the scrollbars inside the content area, * without increasing the padding. The scrollbars will be overlaid with @@ -1020,6 +1052,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ static final int PARENT_SAVE_DISABLED_MASK = 0x20000000; + /** @hide */ + @IntDef(flag = true, + value = { + FOCUSABLES_ALL, + FOCUSABLES_TOUCH_MODE + }) + @Retention(RetentionPolicy.SOURCE) + public @interface FocusableMode {} + /** * View flag indicating whether {@link #addFocusables(ArrayList, int, int)} * should add all focusable Views regardless if they are focusable in touch mode. @@ -1032,6 +1073,28 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public static final int FOCUSABLES_TOUCH_MODE = 0x00000001; + /** @hide */ + @IntDef({ + FOCUS_BACKWARD, + FOCUS_FORWARD, + FOCUS_LEFT, + FOCUS_UP, + FOCUS_RIGHT, + FOCUS_DOWN + }) + @Retention(RetentionPolicy.SOURCE) + public @interface FocusDirection {} + + /** @hide */ + @IntDef({ + FOCUS_LEFT, + FOCUS_UP, + FOCUS_RIGHT, + FOCUS_DOWN + }) + @Retention(RetentionPolicy.SOURCE) + public @interface FocusRealDirection {} // Like @FocusDirection, but without forward/backward + /** * Use with {@link #focusSearch(int)}. Move focus to the previous selectable * item. @@ -1587,7 +1650,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see #setTag(Object) * @see #getTag() */ - protected Object mTag; + protected Object mTag = null; // for mPrivateFlags: /** {@hide} */ @@ -1729,12 +1792,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private static final int PFLAG_HOVERED = 0x10000000; /** - * Indicates that pivotX or pivotY were explicitly set and we should not assume the center - * for transform operations - * - * @hide + * no longer needed, should be reused */ - private static final int PFLAG_PIVOT_EXPLICITLY_SET = 0x20000000; + private static final int PFLAG_DOES_NOTHING_REUSE_PLEASE = 0x20000000; /** {@hide} */ static final int PFLAG_ACTIVATED = 0x40000000; @@ -1804,6 +1864,25 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ static final int PFLAG2_DRAG_HOVERED = 0x00000002; + /** @hide */ + @IntDef({ + LAYOUT_DIRECTION_LTR, + LAYOUT_DIRECTION_RTL, + LAYOUT_DIRECTION_INHERIT, + LAYOUT_DIRECTION_LOCALE + }) + @Retention(RetentionPolicy.SOURCE) + // Not called LayoutDirection to avoid conflict with android.util.LayoutDirection + public @interface LayoutDir {} + + /** @hide */ + @IntDef({ + LAYOUT_DIRECTION_LTR, + LAYOUT_DIRECTION_RTL + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ResolvedLayoutDir {} + /** * Horizontal layout direction of this view is from Left to Right. * Use with {@link #setLayoutDirection}. @@ -1982,7 +2061,20 @@ public class View implements Drawable.Callback, KeyEvent.Callback, static final int PFLAG2_TEXT_DIRECTION_RESOLVED_DEFAULT = TEXT_DIRECTION_RESOLVED_DEFAULT << PFLAG2_TEXT_DIRECTION_RESOLVED_MASK_SHIFT; - /* + /** @hide */ + @IntDef({ + TEXT_ALIGNMENT_INHERIT, + TEXT_ALIGNMENT_GRAVITY, + TEXT_ALIGNMENT_CENTER, + TEXT_ALIGNMENT_TEXT_START, + TEXT_ALIGNMENT_TEXT_END, + TEXT_ALIGNMENT_VIEW_START, + TEXT_ALIGNMENT_VIEW_END + }) + @Retention(RetentionPolicy.SOURCE) + public @interface TextAlignment {} + + /** * Default text alignment. The text alignment of this View is inherited from its parent. * Use with {@link #setTextAlignment(int)} */ @@ -2234,7 +2326,17 @@ public class View implements Drawable.Callback, KeyEvent.Callback, /* End of masks for mPrivateFlags2 */ - /* Masks for mPrivateFlags3 */ + /** + * Masks for mPrivateFlags3, as generated by dumpFlags(): + * + * |-------|-------|-------|-------| + * 1 PFLAG3_VIEW_IS_ANIMATING_TRANSFORM + * 1 PFLAG3_VIEW_IS_ANIMATING_ALPHA + * 1 PFLAG3_IS_LAID_OUT + * 1 PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT + * 1 PFLAG3_CALLED_SUPER + * |-------|-------|-------|-------| + */ /** * Flag indicating that view has a transform animation set on it. This is used to track whether @@ -2268,16 +2370,25 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ static final int PFLAG3_CALLED_SUPER = 0x10; + /** + * Flag indicating that an view will be clipped to its outline. + */ + static final int PFLAG3_CLIP_TO_OUTLINE = 0x20; + + /** + * Flag indicating that a view's outline has been specifically defined. + */ + static final int PFLAG3_OUTLINE_DEFINED = 0x40; /** * Flag indicating that we're in the process of applying window insets. */ - static final int PFLAG3_APPLYING_INSETS = 0x40; + static final int PFLAG3_APPLYING_INSETS = 0x80; /** * Flag indicating that we're in the process of fitting system windows using the old method. */ - static final int PFLAG3_FITTING_SYSTEM_WINDOWS = 0x80; + static final int PFLAG3_FITTING_SYSTEM_WINDOWS = 0x100; /* End of masks for mPrivateFlags3 */ @@ -2674,6 +2785,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; + /** @hide */ + @IntDef(flag = true, + value = { FIND_VIEWS_WITH_TEXT, FIND_VIEWS_WITH_CONTENT_DESCRIPTION }) + @Retention(RetentionPolicy.SOURCE) + public @interface FindViewFlags {} + /** * Find views that render the specified text. * @@ -2695,7 +2812,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * added and it is a responsibility of the client to call the APIs of * the provider to determine whether the virtual tree rooted at this View * contains the text, i.e. getting the list of {@link AccessibilityNodeInfo}s - * represeting the virtual views with this text. + * representing the virtual views with this text. * * @see #findViewsWithText(ArrayList, CharSequence, int) * @@ -2810,120 +2927,23 @@ public class View implements Drawable.Callback, KeyEvent.Callback, static class TransformationInfo { /** * The transform matrix for the View. This transform is calculated internally - * based on the rotation, scaleX, and scaleY properties. The identity matrix - * is used by default. Do *not* use this variable directly; instead call - * getMatrix(), which will automatically recalculate the matrix if necessary - * to get the correct matrix based on the latest rotation and scale properties. + * based on the translation, rotation, and scale properties. + * + * Do *not* use this variable directly; instead call getMatrix(), which will + * load the value from the View's RenderNode. */ private final Matrix mMatrix = new Matrix(); /** - * The transform matrix for the View. This transform is calculated internally - * based on the rotation, scaleX, and scaleY properties. The identity matrix - * is used by default. Do *not* use this variable directly; instead call - * getInverseMatrix(), which will automatically recalculate the matrix if necessary - * to get the correct matrix based on the latest rotation and scale properties. + * The inverse transform matrix for the View. This transform is calculated + * internally based on the translation, rotation, and scale properties. + * + * Do *not* use this variable directly; instead call getInverseMatrix(), + * which will load the value from the View's RenderNode. */ private Matrix mInverseMatrix; /** - * An internal variable that tracks whether we need to recalculate the - * transform matrix, based on whether the rotation or scaleX/Y properties - * have changed since the matrix was last calculated. - */ - boolean mMatrixDirty = false; - - /** - * An internal variable that tracks whether we need to recalculate the - * transform matrix, based on whether the rotation or scaleX/Y properties - * have changed since the matrix was last calculated. - */ - private boolean mInverseMatrixDirty = true; - - /** - * A variable that tracks whether we need to recalculate the - * transform matrix, based on whether the rotation or scaleX/Y properties - * have changed since the matrix was last calculated. This variable - * is only valid after a call to updateMatrix() or to a function that - * calls it such as getMatrix(), hasIdentityMatrix() and getInverseMatrix(). - */ - private boolean mMatrixIsIdentity = true; - - /** - * The Camera object is used to compute a 3D matrix when rotationX or rotationY are set. - */ - private Camera mCamera = null; - - /** - * This matrix is used when computing the matrix for 3D rotations. - */ - private Matrix matrix3D = null; - - /** - * These prev values are used to recalculate a centered pivot point when necessary. The - * pivot point is only used in matrix operations (when rotation, scale, or translation are - * set), so thes values are only used then as well. - */ - private int mPrevWidth = -1; - private int mPrevHeight = -1; - - /** - * The degrees rotation around the vertical axis through the pivot point. - */ - @ViewDebug.ExportedProperty - float mRotationY = 0f; - - /** - * The degrees rotation around the horizontal axis through the pivot point. - */ - @ViewDebug.ExportedProperty - float mRotationX = 0f; - - /** - * The degrees rotation around the pivot point. - */ - @ViewDebug.ExportedProperty - float mRotation = 0f; - - /** - * The amount of translation of the object away from its left property (post-layout). - */ - @ViewDebug.ExportedProperty - float mTranslationX = 0f; - - /** - * The amount of translation of the object away from its top property (post-layout). - */ - @ViewDebug.ExportedProperty - float mTranslationY = 0f; - - /** - * The amount of scale in the x direction around the pivot point. A - * value of 1 means no scaling is applied. - */ - @ViewDebug.ExportedProperty - float mScaleX = 1f; - - /** - * The amount of scale in the y direction around the pivot point. A - * value of 1 means no scaling is applied. - */ - @ViewDebug.ExportedProperty - float mScaleY = 1f; - - /** - * The x location of the point around which the view is rotated and scaled. - */ - @ViewDebug.ExportedProperty - float mPivotX = 0f; - - /** - * The y location of the point around which the view is rotated and scaled. - */ - @ViewDebug.ExportedProperty - float mPivotY = 0f; - - /** * The opacity of the View. This is a value from 0 to 1, where 0 means * completely transparent and 1 means completely opaque. */ @@ -2948,12 +2968,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private boolean mLastIsOpaque; /** - * Convenience value to check for float values that are close enough to zero to be considered - * zero. - */ - private static final float NONZERO_EPSILON = .001f; - - /** * The distance in pixels from the left edge of this view's parent * to the left edge of this view. * {@hide} @@ -3135,6 +3149,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @ViewDebug.ExportedProperty(deepExport = true, prefix = "bg_") private Drawable mBackground; + /** + * Display list used for backgrounds. + * <p> + * When non-null and valid, this is expected to contain an up-to-date copy + * of the background drawable. It is cleared on temporary detach and reset + * on cleanup. + */ + private RenderNode mBackgroundDisplayList; + private int mBackgroundResource; private boolean mBackgroundSizeChanged; @@ -3208,6 +3231,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private int[] mDrawableState = null; /** + * Stores the outline of the view, passed down to the DisplayList level for + * defining shadow shape and clipping. + * + * TODO: once RenderNode is long-lived, remove this and rely on native copy. + */ + private Outline mOutline; + + /** * When this view has focus and the next focus is {@link #FOCUS_LEFT}, * the user may specify which view to go to next. */ @@ -3410,7 +3441,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private Bitmap mDrawingCache; private Bitmap mUnscaledDrawingCache; - DisplayList mDisplayList; + /** + * RenderNode holding View properties, potentially holding a DisplayList of View content. + * <p> + * When non-null and valid, this is expected to contain an up-to-date copy + * of the View content. Its DisplayList content is cleared on temporary detach and reset on + * cleanup. + */ + final RenderNode mRenderNode; /** * Set to true when the view is sending hover accessibility events because it @@ -3461,6 +3499,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, setOverScrollMode(OVER_SCROLL_IF_CONTENT_SCROLLS); mUserPaddingStart = UNDEFINED_PADDING; mUserPaddingEnd = UNDEFINED_PADDING; + mRenderNode = RenderNode.create(getClass().getName()); if (!sCompatibilityDone && context != null) { final int targetSdkVersion = context.getApplicationInfo().targetSdkVersion; @@ -3497,27 +3536,64 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** - * Perform inflation from XML and apply a class-specific base style. This - * constructor of View allows subclasses to use their own base style when - * they are inflating. For example, a Button class's constructor would call - * this version of the super class constructor and supply - * <code>R.attr.buttonStyle</code> for <var>defStyle</var>; this allows - * the theme's button style to modify all of the base view attributes (in - * particular its background) as well as the Button class's attributes. + * Perform inflation from XML and apply a class-specific base style from a + * theme attribute. This constructor of View allows subclasses to use their + * own base style when they are inflating. For example, a Button class's + * constructor would call this version of the super class constructor and + * supply <code>R.attr.buttonStyle</code> for <var>defStyleAttr</var>; this + * allows the theme's button style to modify all of the base view attributes + * (in particular its background) as well as the Button class's attributes. * * @param context The Context the view is running in, through which it can * access the current theme, resources, etc. * @param attrs The attributes of the XML tag that is inflating the view. * @param defStyleAttr An attribute in the current theme that contains a - * reference to a style resource to apply to this view. If 0, no - * default style will be applied. + * reference to a style resource that supplies default values for + * the view. Can be 0 to not look for defaults. * @see #View(Context, AttributeSet) */ public View(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + /** + * Perform inflation from XML and apply a class-specific base style from a + * theme attribute or style resource. This constructor of View allows + * subclasses to use their own base style when they are inflating. + * <p> + * When determining the final value of a particular attribute, there are + * four inputs that come into play: + * <ol> + * <li>Any attribute values in the given AttributeSet. + * <li>The style resource specified in the AttributeSet (named "style"). + * <li>The default style specified by <var>defStyleAttr</var>. + * <li>The default style specified by <var>defStyleRes</var>. + * <li>The base values in this theme. + * </ol> + * <p> + * Each of these inputs is considered in-order, with the first listed taking + * precedence over the following ones. In other words, if in the + * AttributeSet you have supplied <code><Button * textColor="#ff000000"></code> + * , then the button's text will <em>always</em> be black, regardless of + * what is specified in any of the styles. + * + * @param context The Context the view is running in, through which it can + * access the current theme, resources, etc. + * @param attrs The attributes of the XML tag that is inflating the view. + * @param defStyleAttr An attribute in the current theme that contains a + * reference to a style resource that supplies default values for + * the view. Can be 0 to not look for defaults. + * @param defStyleRes A resource identifier of a style resource that + * supplies default values for the view, used only if + * defStyleAttr is 0 or can not be found in the theme. Can be 0 + * to not look for defaults. + * @see #View(Context, AttributeSet, int) + */ + public View(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { this(context); - TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.View, - defStyleAttr, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes); Drawable background = null; @@ -3540,6 +3616,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, float tx = 0; float ty = 0; + float tz = 0; float rotation = 0; float rotationX = 0; float rotationY = 0; @@ -3619,6 +3696,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, ty = a.getDimensionPixelOffset(attr, 0); transformSet = true; break; + case com.android.internal.R.styleable.View_translationZ: + tz = a.getDimensionPixelOffset(attr, 0); + transformSet = true; + break; case com.android.internal.R.styleable.View_rotation: rotation = a.getFloat(attr, 0); transformSet = true; @@ -3871,6 +3952,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, case R.styleable.View_accessibilityLiveRegion: setAccessibilityLiveRegion(a.getInt(attr, ACCESSIBILITY_LIVE_REGION_DEFAULT)); break; + case R.styleable.View_sharedElementName: + setSharedElementName(a.getString(attr)); + break; } } @@ -3960,6 +4044,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (transformSet) { setTranslationX(tx); setTranslationY(ty); + setTranslationZ(tz); setRotation(rotation); setRotationX(rotationX); setRotationY(rotationY); @@ -3979,6 +4064,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ View() { mResources = null; + mRenderNode = RenderNode.create(getClass().getName()); } public String toString() { @@ -4026,7 +4112,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, out.append(" #"); out.append(Integer.toHexString(id)); final Resources r = mResources; - if (id != 0 && r != null) { + if (Resources.resourceHasPackage(id) && r != null) { try { String pkgname; switch (id&0xff000000) { @@ -4608,7 +4694,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @param previouslyFocusedRect The rectangle of the view that had focus * prior in this View's coordinate system. */ - void handleFocusGainInternal(int direction, Rect previouslyFocusedRect) { + void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) { if (DBG) { System.out.println(this + " requestFocus()"); } @@ -4626,12 +4712,43 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this); } + manageFocusHotspot(true, oldFocus); onFocusChanged(true, direction, previouslyFocusedRect); refreshDrawableState(); } } /** + * Forwards focus information to the background drawable, if necessary. When + * the view is gaining focus, <code>v</code> is the previous focus holder. + * When the view is losing focus, <code>v</code> is the next focus holder. + * + * @param focused whether this view is focused + * @param v previous or the next focus holder, or null if none + */ + private void manageFocusHotspot(boolean focused, View v) { + if (mBackground != null && mBackground.supportsHotspots()) { + final Rect r = new Rect(); + if (v != null) { + v.getBoundsOnScreen(r); + final int[] location = new int[2]; + getLocationOnScreen(location); + r.offset(-location[0], -location[1]); + } else { + r.set(mLeft, mTop, mRight, mBottom); + } + + final float x = r.exactCenterX(); + final float y = r.exactCenterY(); + mBackground.setHotspot(R.attr.state_focused, x, y); + + if (!focused) { + mBackground.removeHotspot(R.attr.state_focused); + } + } + } + + /** * Request that a rectangle of this view be visible on the screen, * scrolling if necessary just enough. * @@ -4717,7 +4834,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, System.out.println(this + " clearFocus()"); } - clearFocusInternal(true, true); + clearFocusInternal(null, true, true); } /** @@ -4729,10 +4846,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @param refocus when propagate is true, specifies whether to request the * root view place new focus */ - void clearFocusInternal(boolean propagate, boolean refocus) { + void clearFocusInternal(View focused, boolean propagate, boolean refocus) { if ((mPrivateFlags & PFLAG_FOCUSED) != 0) { mPrivateFlags &= ~PFLAG_FOCUSED; + if (hasFocus()) { + manageFocusHotspot(false, focused); + } + if (propagate && mParent != null) { mParent.clearChildFocus(this); } @@ -4766,12 +4887,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * after calling this method. Otherwise, the view hierarchy may be left in * an inconstent state. */ - void unFocus() { + void unFocus(View focused) { if (DBG) { System.out.println(this + " unFocus()"); } - clearFocusInternal(false, false); + clearFocusInternal(focused, false, false); } /** @@ -4819,7 +4940,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * passed in as finer grained information about where the focus is coming * from (in addition to direction). Will be <code>null</code> otherwise. */ - protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { + protected void onFocusChanged(boolean gainFocus, @FocusDirection int direction, + @Nullable Rect previouslyFocusedRect) { if (gainFocus) { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); } else { @@ -4881,10 +5003,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see AccessibilityDelegate */ public void sendAccessibilityEvent(int eventType) { - // Excluded views do not send accessibility events. - if (!includeForAccessibility()) { - return; - } if (mAccessibilityDelegate != null) { mAccessibilityDelegate.sendAccessibilityEvent(this, eventType); } else { @@ -5502,7 +5620,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @hide */ public int getAccessibilityWindowId() { - return mAttachInfo != null ? mAttachInfo.mAccessibilityWindowId : NO_ID; + return mAttachInfo != null ? mAttachInfo.mAccessibilityWindowId + : AccessibilityNodeInfo.UNDEFINED_ITEM_ID; } /** @@ -5679,6 +5798,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @attr ref android.R.styleable#View_drawingCacheQuality */ + @DrawingCacheQuality public int getDrawingCacheQuality() { return mViewFlags & DRAWING_CACHE_QUALITY_MASK; } @@ -5696,7 +5816,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @attr ref android.R.styleable#View_drawingCacheQuality */ - public void setDrawingCacheQuality(int quality) { + public void setDrawingCacheQuality(@DrawingCacheQuality int quality) { setFlags(quality, DRAWING_CACHE_QUALITY_MASK); } @@ -5913,7 +6033,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @return {@code true} if this view applied the insets and it should not * continue propagating further down the hierarchy, {@code false} otherwise. * @see #getFitsSystemWindows() - * @see #setFitsSystemWindows(boolean) + * @see #setFitsSystemWindows(boolean) * @see #setSystemUiVisibility(int) * * @deprecated As of API XX use {@link #dispatchApplyWindowInsets(WindowInsets)} to apply @@ -5930,7 +6050,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mPrivateFlags3 |= PFLAG3_FITTING_SYSTEM_WINDOWS; return !dispatchApplyWindowInsets(new WindowInsets(insets)).hasInsets(); } finally { - mPrivateFlags3 &= PFLAG3_FITTING_SYSTEM_WINDOWS; + mPrivateFlags3 &= ~PFLAG3_FITTING_SYSTEM_WINDOWS; } } else { // We're being called from the newer apply insets path. @@ -6155,6 +6275,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @ViewDebug.IntToString(from = INVISIBLE, to = "INVISIBLE"), @ViewDebug.IntToString(from = GONE, to = "GONE") }) + @Visibility public int getVisibility() { return mViewFlags & VISIBILITY_MASK; } @@ -6166,7 +6287,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @attr ref android.R.styleable#View_visibility */ @RemotableViewMethod - public void setVisibility(int visibility) { + public void setVisibility(@Visibility int visibility) { setFlags(visibility, VISIBILITY_MASK); if (mBackground != null) mBackground.setVisible(visibility == VISIBLE, false); } @@ -6325,6 +6446,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @ViewDebug.IntToString(from = LAYOUT_DIRECTION_INHERIT, to = "INHERIT"), @ViewDebug.IntToString(from = LAYOUT_DIRECTION_LOCALE, to = "LOCALE") }) + @LayoutDir public int getRawLayoutDirection() { return (mPrivateFlags2 & PFLAG2_LAYOUT_DIRECTION_MASK) >> PFLAG2_LAYOUT_DIRECTION_MASK_SHIFT; } @@ -6347,7 +6469,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @attr ref android.R.styleable#View_layoutDirection */ @RemotableViewMethod - public void setLayoutDirection(int layoutDirection) { + public void setLayoutDirection(@LayoutDir int layoutDirection) { if (getRawLayoutDirection() != layoutDirection) { // Reset the current layout direction and the resolved one mPrivateFlags2 &= ~PFLAG2_LAYOUT_DIRECTION_MASK; @@ -6377,6 +6499,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @ViewDebug.IntToString(from = LAYOUT_DIRECTION_LTR, to = "RESOLVED_DIRECTION_LTR"), @ViewDebug.IntToString(from = LAYOUT_DIRECTION_RTL, to = "RESOLVED_DIRECTION_RTL") }) + @ResolvedLayoutDir public int getLayoutDirection() { final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion; if (targetSdkVersion < JELLY_BEAN_MR1) { @@ -6746,7 +6869,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @return The nearest focusable in the specified direction, or null if none * can be found. */ - public View focusSearch(int direction) { + public View focusSearch(@FocusRealDirection int direction) { if (mParent != null) { return mParent.focusSearch(this, direction); } else { @@ -6765,7 +6888,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * FOCUS_DOWN, FOCUS_LEFT, and FOCUS_RIGHT. * @return True if the this view consumed this unhandled move. */ - public boolean dispatchUnhandledMove(View focused, int direction) { + public boolean dispatchUnhandledMove(View focused, @FocusRealDirection int direction) { return false; } @@ -6777,7 +6900,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * or FOCUS_BACKWARD. * @return The user specified next view, or null if there is none. */ - View findUserSetNextFocus(View root, int direction) { + View findUserSetNextFocus(View root, @FocusDirection int direction) { switch (direction) { case FOCUS_LEFT: if (mNextFocusLeftId == View.NO_ID) return null; @@ -6827,7 +6950,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @param direction The direction of the focus * @return A list of focusable views */ - public ArrayList<View> getFocusables(int direction) { + public ArrayList<View> getFocusables(@FocusDirection int direction) { ArrayList<View> result = new ArrayList<View>(24); addFocusables(result, direction); return result; @@ -6841,7 +6964,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @param views Focusable views found so far * @param direction The direction of the focus */ - public void addFocusables(ArrayList<View> views, int direction) { + public void addFocusables(ArrayList<View> views, @FocusDirection int direction) { addFocusables(views, direction, FOCUSABLES_TOUCH_MODE); } @@ -6861,7 +6984,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see #FOCUSABLES_ALL * @see #FOCUSABLES_TOUCH_MODE */ - public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { + public void addFocusables(ArrayList<View> views, @FocusDirection int direction, + @FocusableMode int focusableMode) { if (views == null) { return; } @@ -6890,7 +7014,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see #FIND_VIEWS_WITH_CONTENT_DESCRIPTION * @see #setContentDescription(CharSequence) */ - public void findViewsWithText(ArrayList<View> outViews, CharSequence searched, int flags) { + public void findViewsWithText(ArrayList<View> outViews, CharSequence searched, + @FindViewFlags int flags) { if (getAccessibilityNodeProvider() != null) { if ((flags & FIND_VIEWS_WITH_ACCESSIBILITY_NODE_PROVIDERS) != 0) { outViews.add(this); @@ -6937,7 +7062,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * Returns whether this View is accessibility focused. * * @return True if this View is accessibility focused. - * @hide */ public boolean isAccessibilityFocused() { return (mPrivateFlags2 & PFLAG2_ACCESSIBILITY_FOCUSED) != 0; @@ -7285,11 +7409,38 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** - * Gets whether this view should be exposed for accessibility. + * Computes whether this view should be exposed for accessibility. In + * general, views that are interactive or provide information are exposed + * while views that serve only as containers are hidden. + * <p> + * If an ancestor of this view has importance + * {@link #IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS}, this method + * returns <code>false</code>. + * <p> + * Otherwise, the value is computed according to the view's + * {@link #getImportantForAccessibility()} value: + * <ol> + * <li>{@link #IMPORTANT_FOR_ACCESSIBILITY_NO} or + * {@link #IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS}, return <code>false + * </code> + * <li>{@link #IMPORTANT_FOR_ACCESSIBILITY_YES}, return <code>true</code> + * <li>{@link #IMPORTANT_FOR_ACCESSIBILITY_AUTO}, return <code>true</code> if + * view satisfies any of the following: + * <ul> + * <li>Is actionable, e.g. {@link #isClickable()}, + * {@link #isLongClickable()}, or {@link #isFocusable()} + * <li>Has an {@link AccessibilityDelegate} + * <li>Has an interaction listener, e.g. {@link OnTouchListener}, + * {@link OnKeyListener}, etc. + * <li>Is an accessibility live region, e.g. + * {@link #getAccessibilityLiveRegion()} is not + * {@link #ACCESSIBILITY_LIVE_REGION_NONE}. + * </ul> + * </ol> * * @return Whether the view is exposed for accessibility. - * - * @hide + * @see #setImportantForAccessibility(int) + * @see #getImportantForAccessibility() */ public boolean isImportantForAccessibility() { final int mode = (mPrivateFlags2 & PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_MASK) @@ -7342,9 +7493,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @param children The list of children for accessibility. */ public void addChildrenForAccessibility(ArrayList<View> children) { - if (includeForAccessibility()) { - children.add(this); - } + } /** @@ -7358,7 +7507,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @hide */ public boolean includeForAccessibility() { - //noinspection SimplifiableIfStatement if (mAttachInfo != null) { return (mAttachInfo.mAccessibilityFetchFlags & AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS) != 0 @@ -7381,7 +7529,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** - * Returns whether the View has registered callbacks wich makes it + * Returns whether the View has registered callbacks which makes it * important for accessibility. * * @return True if the view is actionable for accessibility. @@ -7400,7 +7548,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * notification is at at most once every * {@link ViewConfiguration#getSendRecurringAccessibilityEventsInterval()} * to avoid unnecessary load to the system. Also once a view has a pending - * notifucation this method is a NOP until the notification has been sent. + * notification this method is a NOP until the notification has been sent. * * @hide */ @@ -7718,8 +7866,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @hide */ public void dispatchStartTemporaryDetach() { - clearDisplayList(); - onStartTemporaryDetach(); } @@ -8080,7 +8226,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @param visibility The new visibility of changedView: {@link #VISIBLE}, * {@link #INVISIBLE} or {@link #GONE}. */ - protected void dispatchVisibilityChanged(View changedView, int visibility) { + protected void dispatchVisibilityChanged(@NonNull View changedView, + @Visibility int visibility) { onVisibilityChanged(changedView, visibility); } @@ -8091,7 +8238,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @param visibility The new visibility of changedView: {@link #VISIBLE}, * {@link #INVISIBLE} or {@link #GONE}. */ - protected void onVisibilityChanged(View changedView, int visibility) { + protected void onVisibilityChanged(@NonNull View changedView, @Visibility int visibility) { if (visibility == VISIBLE) { if (mAttachInfo != null) { initialAwakenScrollBars(); @@ -8110,7 +8257,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @param hint A hint about whether or not this view is displayed: * {@link #VISIBLE} or {@link #INVISIBLE}. */ - public void dispatchDisplayHint(int hint) { + public void dispatchDisplayHint(@Visibility int hint) { onDisplayHint(hint); } @@ -8123,7 +8270,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @param hint A hint about whether or not this view is displayed: * {@link #VISIBLE} or {@link #INVISIBLE}. */ - protected void onDisplayHint(int hint) { + protected void onDisplayHint(@Visibility int hint) { } /** @@ -8134,7 +8281,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @see #onWindowVisibilityChanged(int) */ - public void dispatchWindowVisibilityChanged(int visibility) { + public void dispatchWindowVisibilityChanged(@Visibility int visibility) { onWindowVisibilityChanged(visibility); } @@ -8148,7 +8295,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @param visibility The new visibility of the window. */ - protected void onWindowVisibilityChanged(int visibility) { + protected void onWindowVisibilityChanged(@Visibility int visibility) { if (visibility == VISIBLE) { initialAwakenScrollBars(); } @@ -8160,6 +8307,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @return Returns the current visibility of the view's window. */ + @Visibility public int getWindowVisibility() { return mAttachInfo != null ? mAttachInfo.mWindowVisibility : GONE; } @@ -8320,7 +8468,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, public boolean onKeyDown(int keyCode, KeyEvent event) { boolean result = false; - if (event.isConfirmKey()) { + if (KeyEvent.isConfirmKey(keyCode)) { if ((mViewFlags & ENABLED_MASK) == DISABLED) { return true; } @@ -8362,7 +8510,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @param event The KeyEvent object that defines the button action. */ public boolean onKeyUp(int keyCode, KeyEvent event) { - if (event.isConfirmKey()) { + if (KeyEvent.isConfirmKey(keyCode)) { if ((mViewFlags & ENABLED_MASK) == DISABLED) { return true; } @@ -8440,7 +8588,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * <p>When implementing this, you probably also want to implement * {@link #onCheckIsTextEditor()} to indicate you will return a - * non-null InputConnection. + * non-null InputConnection.</p> + * + * <p>Also, take good care to fill in the {@link android.view.inputmethod.EditorInfo} + * object correctly and in its entirety, so that the connected IME can rely + * on its values. For example, {@link android.view.inputmethod.EditorInfo#initialSelStart} + * and {@link android.view.inputmethod.EditorInfo#initialSelEnd} members + * must be filled in with the correct cursor position for IMEs to work correctly + * with your application.</p> * * @param outAttrs Fill in with attribute information about the connection. */ @@ -8629,11 +8784,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, && !pointInView(event.getX(), event.getY()))) { mSendingHoverAccessibilityEvents = false; sendAccessibilityHoverEvent(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); - // If the window does not have input focus we take away accessibility - // focus as soon as the user stop hovering over the view. - if (mAttachInfo != null && !mAttachInfo.mHasWindowFocus) { - getViewRootImpl().setAccessibilityFocus(null, null); - } } } @@ -8756,10 +8906,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @return True if the event was handled, false otherwise. */ public boolean onTouchEvent(MotionEvent event) { + final float x = event.getX(); + final float y = event.getY(); final int viewFlags = mViewFlags; if ((viewFlags & ENABLED_MASK) == DISABLED) { if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { + clearHotspot(R.attr.state_pressed); setPressed(false); } // A disabled view that is clickable still consumes the touch @@ -8792,6 +8945,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, // showed it as pressed. Make it show the pressed // state now (before scheduling the click) to ensure // the user sees it. + setHotspot(R.attr.state_pressed, x, y); setPressed(true); } @@ -8824,7 +8978,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, // If the post failed, unpress right now mUnsetPressedState.run(); } + removeTapCallback(); + } else { + clearHotspot(R.attr.state_pressed); } break; @@ -8845,23 +9002,26 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } + mPendingCheckForTap.x = event.getX(); + mPendingCheckForTap.y = event.getY(); postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { // Not inside a scrolling container, so show the feedback right away + setHotspot(R.attr.state_pressed, x, y); setPressed(true); checkForLongClick(0); } break; case MotionEvent.ACTION_CANCEL: + clearHotspot(R.attr.state_pressed); setPressed(false); removeTapCallback(); removeLongPressCallback(); break; case MotionEvent.ACTION_MOVE: - final int x = (int) event.getX(); - final int y = (int) event.getY(); + setHotspot(R.attr.state_pressed, x, y); // Be lenient about moving outside of buttons if (!pointInView(x, y, mTouchSlop)) { @@ -8876,12 +9036,27 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } break; } + return true; } return false; } + private void setHotspot(int id, float x, float y) { + final Drawable bg = mBackground; + if (bg != null && bg.supportsHotspots()) { + bg.setHotspot(id, x, y); + } + } + + private void clearHotspot(int id) { + final Drawable bg = mBackground; + if (bg != null && bg.supportsHotspots()) { + bg.removeHotspot(id); + } + } + /** * @hide */ @@ -8919,6 +9094,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ private void removeUnsetPressCallback() { if ((mPrivateFlags & PFLAG_PRESSED) != 0 && mUnsetPressedState != null) { + clearHotspot(R.attr.state_pressed); setPressed(false); removeCallbacks(mUnsetPressedState); } @@ -9081,7 +9257,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if ((changed & VISIBILITY_MASK) != 0) { // If the view is invisible, cleanup its display list to free up resources - if (newVisibility != VISIBLE) { + if (newVisibility != VISIBLE && mAttachInfo != null) { cleanupDraw(); } @@ -9093,6 +9269,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mParent.invalidateChild(this, null); } dispatchVisibilityChanged(this, newVisibility); + + notifySubtreeAccessibilityStateChangedIfNeeded(); } if ((changed & WILL_NOT_CACHE_DRAWING) != 0) { @@ -9395,21 +9573,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @return The current transform matrix for the view */ public Matrix getMatrix() { - if (mTransformationInfo != null) { - updateMatrix(); - return mTransformationInfo.mMatrix; - } - return Matrix.IDENTITY_MATRIX; - } - - /** - * Utility function to determine if the value is far enough away from zero to be - * considered non-zero. - * @param value A floating point value to check for zero-ness - * @return whether the passed-in value is far enough away from zero to be considered non-zero - */ - private static boolean nonzero(float value) { - return (value < -NONZERO_EPSILON || value > NONZERO_EPSILON); + ensureTransformationInfo(); + final Matrix matrix = mTransformationInfo.mMatrix; + mRenderNode.getMatrix(matrix); + return matrix; } /** @@ -9419,11 +9586,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @return True if the transform matrix is the identity matrix, false otherwise. */ final boolean hasIdentityMatrix() { - if (mTransformationInfo != null) { - updateMatrix(); - return mTransformationInfo.mMatrixIsIdentity; - } - return true; + return mRenderNode.hasIdentityMatrix(); } void ensureTransformationInfo() { @@ -9432,53 +9595,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } } - /** - * Recomputes the transform matrix if necessary. - */ - private void updateMatrix() { - final TransformationInfo info = mTransformationInfo; - if (info == null) { - return; - } - if (info.mMatrixDirty) { - // transform-related properties have changed since the last time someone - // asked for the matrix; recalculate it with the current values - - // Figure out if we need to update the pivot point - if ((mPrivateFlags & PFLAG_PIVOT_EXPLICITLY_SET) == 0) { - if ((mRight - mLeft) != info.mPrevWidth || (mBottom - mTop) != info.mPrevHeight) { - info.mPrevWidth = mRight - mLeft; - info.mPrevHeight = mBottom - mTop; - info.mPivotX = info.mPrevWidth / 2f; - info.mPivotY = info.mPrevHeight / 2f; - } - } - info.mMatrix.reset(); - if (!nonzero(info.mRotationX) && !nonzero(info.mRotationY)) { - info.mMatrix.setTranslate(info.mTranslationX, info.mTranslationY); - info.mMatrix.preRotate(info.mRotation, info.mPivotX, info.mPivotY); - info.mMatrix.preScale(info.mScaleX, info.mScaleY, info.mPivotX, info.mPivotY); - } else { - if (info.mCamera == null) { - info.mCamera = new Camera(); - info.matrix3D = new Matrix(); - } - info.mCamera.save(); - info.mMatrix.preScale(info.mScaleX, info.mScaleY, info.mPivotX, info.mPivotY); - info.mCamera.rotate(info.mRotationX, info.mRotationY, -info.mRotation); - info.mCamera.getMatrix(info.matrix3D); - info.matrix3D.preTranslate(-info.mPivotX, -info.mPivotY); - info.matrix3D.postTranslate(info.mPivotX + info.mTranslationX, - info.mPivotY + info.mTranslationY); - info.mMatrix.postConcat(info.matrix3D); - info.mCamera.restore(); - } - info.mMatrixDirty = false; - info.mMatrixIsIdentity = info.mMatrix.isIdentity(); - info.mInverseMatrixDirty = true; - } - } - /** * Utility method to retrieve the inverse of the current mMatrix property. * We cache the matrix to avoid recalculating it when transform properties @@ -9487,19 +9603,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @return The inverse of the current matrix of this view. */ final Matrix getInverseMatrix() { - final TransformationInfo info = mTransformationInfo; - if (info != null) { - updateMatrix(); - if (info.mInverseMatrixDirty) { - if (info.mInverseMatrix == null) { - info.mInverseMatrix = new Matrix(); - } - info.mMatrix.invert(info.mInverseMatrix); - info.mInverseMatrixDirty = false; - } - return info.mInverseMatrix; + ensureTransformationInfo(); + if (mTransformationInfo.mInverseMatrix == null) { + mTransformationInfo.mInverseMatrix = new Matrix(); } - return Matrix.IDENTITY_MATRIX; + final Matrix matrix = mTransformationInfo.mInverseMatrix; + mRenderNode.getInverseMatrix(matrix); + return matrix; } /** @@ -9510,14 +9620,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @return The distance along the Z axis. */ public float getCameraDistance() { - ensureTransformationInfo(); final float dpi = mResources.getDisplayMetrics().densityDpi; - final TransformationInfo info = mTransformationInfo; - if (info.mCamera == null) { - info.mCamera = new Camera(); - info.matrix3D = new Matrix(); - } - return -(info.mCamera.getLocationZ() * dpi); + return -(mRenderNode.getCameraDistance() * dpi); } /** @@ -9560,27 +9664,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see #setRotationY(float) */ public void setCameraDistance(float distance) { - invalidateViewProperty(true, false); - - ensureTransformationInfo(); final float dpi = mResources.getDisplayMetrics().densityDpi; - final TransformationInfo info = mTransformationInfo; - if (info.mCamera == null) { - info.mCamera = new Camera(); - info.matrix3D = new Matrix(); - } - - info.mCamera.setLocation(0.0f, 0.0f, -Math.abs(distance) / dpi); - info.mMatrixDirty = true; + invalidateViewProperty(true, false); + mRenderNode.setCameraDistance(-Math.abs(distance) / dpi); invalidateViewProperty(false, false); - if (mDisplayList != null) { - mDisplayList.setCameraDistance(-Math.abs(distance) / dpi); - } - if ((mPrivateFlags2 & PFLAG2_VIEW_QUICK_REJECTED) == PFLAG2_VIEW_QUICK_REJECTED) { - // View was rejected last time it was drawn by its parent; this may have changed - invalidateParentIfNeeded(); - } + + invalidateParentIfNeededAndWasQuickRejected(); } /** @@ -9594,7 +9684,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ @ViewDebug.ExportedProperty(category = "drawing") public float getRotation() { - return mTransformationInfo != null ? mTransformationInfo.mRotation : 0; + return mRenderNode.getRotation(); } /** @@ -9612,21 +9702,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @attr ref android.R.styleable#View_rotation */ public void setRotation(float rotation) { - ensureTransformationInfo(); - final TransformationInfo info = mTransformationInfo; - if (info.mRotation != rotation) { + if (rotation != getRotation()) { // Double-invalidation is necessary to capture view's old and new areas invalidateViewProperty(true, false); - info.mRotation = rotation; - info.mMatrixDirty = true; + mRenderNode.setRotation(rotation); invalidateViewProperty(false, true); - if (mDisplayList != null) { - mDisplayList.setRotation(rotation); - } - if ((mPrivateFlags2 & PFLAG2_VIEW_QUICK_REJECTED) == PFLAG2_VIEW_QUICK_REJECTED) { - // View was rejected last time it was drawn by its parent; this may have changed - invalidateParentIfNeeded(); - } + + invalidateParentIfNeededAndWasQuickRejected(); } } @@ -9641,7 +9723,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ @ViewDebug.ExportedProperty(category = "drawing") public float getRotationY() { - return mTransformationInfo != null ? mTransformationInfo.mRotationY : 0; + return mRenderNode.getRotationY(); } /** @@ -9664,20 +9746,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @attr ref android.R.styleable#View_rotationY */ public void setRotationY(float rotationY) { - ensureTransformationInfo(); - final TransformationInfo info = mTransformationInfo; - if (info.mRotationY != rotationY) { + if (rotationY != getRotationY()) { invalidateViewProperty(true, false); - info.mRotationY = rotationY; - info.mMatrixDirty = true; + mRenderNode.setRotationY(rotationY); invalidateViewProperty(false, true); - if (mDisplayList != null) { - mDisplayList.setRotationY(rotationY); - } - if ((mPrivateFlags2 & PFLAG2_VIEW_QUICK_REJECTED) == PFLAG2_VIEW_QUICK_REJECTED) { - // View was rejected last time it was drawn by its parent; this may have changed - invalidateParentIfNeeded(); - } + + invalidateParentIfNeededAndWasQuickRejected(); } } @@ -9692,7 +9766,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ @ViewDebug.ExportedProperty(category = "drawing") public float getRotationX() { - return mTransformationInfo != null ? mTransformationInfo.mRotationX : 0; + return mRenderNode.getRotationX(); } /** @@ -9715,20 +9789,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @attr ref android.R.styleable#View_rotationX */ public void setRotationX(float rotationX) { - ensureTransformationInfo(); - final TransformationInfo info = mTransformationInfo; - if (info.mRotationX != rotationX) { + if (rotationX != getRotationX()) { invalidateViewProperty(true, false); - info.mRotationX = rotationX; - info.mMatrixDirty = true; + mRenderNode.setRotationX(rotationX); invalidateViewProperty(false, true); - if (mDisplayList != null) { - mDisplayList.setRotationX(rotationX); - } - if ((mPrivateFlags2 & PFLAG2_VIEW_QUICK_REJECTED) == PFLAG2_VIEW_QUICK_REJECTED) { - // View was rejected last time it was drawn by its parent; this may have changed - invalidateParentIfNeeded(); - } + + invalidateParentIfNeededAndWasQuickRejected(); } } @@ -9744,7 +9810,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ @ViewDebug.ExportedProperty(category = "drawing") public float getScaleX() { - return mTransformationInfo != null ? mTransformationInfo.mScaleX : 1; + return mRenderNode.getScaleX(); } /** @@ -9758,20 +9824,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @attr ref android.R.styleable#View_scaleX */ public void setScaleX(float scaleX) { - ensureTransformationInfo(); - final TransformationInfo info = mTransformationInfo; - if (info.mScaleX != scaleX) { + if (scaleX != getScaleX()) { invalidateViewProperty(true, false); - info.mScaleX = scaleX; - info.mMatrixDirty = true; + mRenderNode.setScaleX(scaleX); invalidateViewProperty(false, true); - if (mDisplayList != null) { - mDisplayList.setScaleX(scaleX); - } - if ((mPrivateFlags2 & PFLAG2_VIEW_QUICK_REJECTED) == PFLAG2_VIEW_QUICK_REJECTED) { - // View was rejected last time it was drawn by its parent; this may have changed - invalidateParentIfNeeded(); - } + + invalidateParentIfNeededAndWasQuickRejected(); } } @@ -9787,7 +9845,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ @ViewDebug.ExportedProperty(category = "drawing") public float getScaleY() { - return mTransformationInfo != null ? mTransformationInfo.mScaleY : 1; + return mRenderNode.getScaleY(); } /** @@ -9801,20 +9859,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @attr ref android.R.styleable#View_scaleY */ public void setScaleY(float scaleY) { - ensureTransformationInfo(); - final TransformationInfo info = mTransformationInfo; - if (info.mScaleY != scaleY) { + if (scaleY != getScaleY()) { invalidateViewProperty(true, false); - info.mScaleY = scaleY; - info.mMatrixDirty = true; + mRenderNode.setScaleY(scaleY); invalidateViewProperty(false, true); - if (mDisplayList != null) { - mDisplayList.setScaleY(scaleY); - } - if ((mPrivateFlags2 & PFLAG2_VIEW_QUICK_REJECTED) == PFLAG2_VIEW_QUICK_REJECTED) { - // View was rejected last time it was drawn by its parent; this may have changed - invalidateParentIfNeeded(); - } + + invalidateParentIfNeededAndWasQuickRejected(); } } @@ -9832,7 +9882,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ @ViewDebug.ExportedProperty(category = "drawing") public float getPivotX() { - return mTransformationInfo != null ? mTransformationInfo.mPivotX : 0; + return mRenderNode.getPivotX(); } /** @@ -9851,23 +9901,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @attr ref android.R.styleable#View_transformPivotX */ public void setPivotX(float pivotX) { - ensureTransformationInfo(); - final TransformationInfo info = mTransformationInfo; - boolean pivotSet = (mPrivateFlags & PFLAG_PIVOT_EXPLICITLY_SET) == - PFLAG_PIVOT_EXPLICITLY_SET; - if (info.mPivotX != pivotX || !pivotSet) { - mPrivateFlags |= PFLAG_PIVOT_EXPLICITLY_SET; + if (mRenderNode.isPivotExplicitlySet() || pivotX != getPivotX()) { invalidateViewProperty(true, false); - info.mPivotX = pivotX; - info.mMatrixDirty = true; + mRenderNode.setPivotX(pivotX); invalidateViewProperty(false, true); - if (mDisplayList != null) { - mDisplayList.setPivotX(pivotX); - } - if ((mPrivateFlags2 & PFLAG2_VIEW_QUICK_REJECTED) == PFLAG2_VIEW_QUICK_REJECTED) { - // View was rejected last time it was drawn by its parent; this may have changed - invalidateParentIfNeeded(); - } + + invalidateParentIfNeededAndWasQuickRejected(); } } @@ -9885,7 +9924,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ @ViewDebug.ExportedProperty(category = "drawing") public float getPivotY() { - return mTransformationInfo != null ? mTransformationInfo.mPivotY : 0; + return mRenderNode.getPivotY(); } /** @@ -9903,23 +9942,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @attr ref android.R.styleable#View_transformPivotY */ public void setPivotY(float pivotY) { - ensureTransformationInfo(); - final TransformationInfo info = mTransformationInfo; - boolean pivotSet = (mPrivateFlags & PFLAG_PIVOT_EXPLICITLY_SET) == - PFLAG_PIVOT_EXPLICITLY_SET; - if (info.mPivotY != pivotY || !pivotSet) { - mPrivateFlags |= PFLAG_PIVOT_EXPLICITLY_SET; + if (mRenderNode.isPivotExplicitlySet() || pivotY != getPivotY()) { invalidateViewProperty(true, false); - info.mPivotY = pivotY; - info.mMatrixDirty = true; + mRenderNode.setPivotY(pivotY); invalidateViewProperty(false, true); - if (mDisplayList != null) { - mDisplayList.setPivotY(pivotY); - } - if ((mPrivateFlags2 & PFLAG2_VIEW_QUICK_REJECTED) == PFLAG2_VIEW_QUICK_REJECTED) { - // View was rejected last time it was drawn by its parent; this may have changed - invalidateParentIfNeeded(); - } + + invalidateParentIfNeededAndWasQuickRejected(); } } @@ -9997,9 +10025,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } else { mPrivateFlags &= ~PFLAG_ALPHA_SET; invalidateViewProperty(true, false); - if (mDisplayList != null) { - mDisplayList.setAlpha(getFinalAlpha()); - } + mRenderNode.setAlpha(getFinalAlpha()); } } } @@ -10024,9 +10050,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return true; } else { mPrivateFlags &= ~PFLAG_ALPHA_SET; - if (mDisplayList != null) { - mDisplayList.setAlpha(getFinalAlpha()); - } + mRenderNode.setAlpha(getFinalAlpha()); } } return false; @@ -10047,9 +10071,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mTransformationInfo.mTransitionAlpha = alpha; mPrivateFlags &= ~PFLAG_ALPHA_SET; invalidateViewProperty(true, false); - if (mDisplayList != null) { - mDisplayList.setAlpha(getFinalAlpha()); - } + mRenderNode.setAlpha(getFinalAlpha()); } } @@ -10096,9 +10118,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public final void setTop(int top) { if (top != mTop) { - updateMatrix(); - final boolean matrixIsIdentity = mTransformationInfo == null - || mTransformationInfo.mMatrixIsIdentity; + final boolean matrixIsIdentity = hasIdentityMatrix(); if (matrixIsIdentity) { if (mAttachInfo != null) { int minTop; @@ -10121,17 +10141,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, int oldHeight = mBottom - mTop; mTop = top; - if (mDisplayList != null) { - mDisplayList.setTop(mTop); - } + mRenderNode.setTop(mTop); sizeChange(width, mBottom - mTop, width, oldHeight); if (!matrixIsIdentity) { - if ((mPrivateFlags & PFLAG_PIVOT_EXPLICITLY_SET) == 0) { - // A change in dimension means an auto-centered pivot point changes, too - mTransformationInfo.mMatrixDirty = true; - } mPrivateFlags |= PFLAG_DRAWN; // force another invalidation with the new orientation invalidate(true); } @@ -10172,9 +10186,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public final void setBottom(int bottom) { if (bottom != mBottom) { - updateMatrix(); - final boolean matrixIsIdentity = mTransformationInfo == null - || mTransformationInfo.mMatrixIsIdentity; + final boolean matrixIsIdentity = hasIdentityMatrix(); if (matrixIsIdentity) { if (mAttachInfo != null) { int maxBottom; @@ -10194,17 +10206,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, int oldHeight = mBottom - mTop; mBottom = bottom; - if (mDisplayList != null) { - mDisplayList.setBottom(mBottom); - } + mRenderNode.setBottom(mBottom); sizeChange(width, mBottom - mTop, width, oldHeight); if (!matrixIsIdentity) { - if ((mPrivateFlags & PFLAG_PIVOT_EXPLICITLY_SET) == 0) { - // A change in dimension means an auto-centered pivot point changes, too - mTransformationInfo.mMatrixDirty = true; - } mPrivateFlags |= PFLAG_DRAWN; // force another invalidation with the new orientation invalidate(true); } @@ -10236,9 +10242,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public final void setLeft(int left) { if (left != mLeft) { - updateMatrix(); - final boolean matrixIsIdentity = mTransformationInfo == null - || mTransformationInfo.mMatrixIsIdentity; + final boolean matrixIsIdentity = hasIdentityMatrix(); if (matrixIsIdentity) { if (mAttachInfo != null) { int minLeft; @@ -10261,17 +10265,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, int height = mBottom - mTop; mLeft = left; - if (mDisplayList != null) { - mDisplayList.setLeft(left); - } + mRenderNode.setLeft(left); sizeChange(mRight - mLeft, height, oldWidth, height); if (!matrixIsIdentity) { - if ((mPrivateFlags & PFLAG_PIVOT_EXPLICITLY_SET) == 0) { - // A change in dimension means an auto-centered pivot point changes, too - mTransformationInfo.mMatrixDirty = true; - } mPrivateFlags |= PFLAG_DRAWN; // force another invalidation with the new orientation invalidate(true); } @@ -10303,9 +10301,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public final void setRight(int right) { if (right != mRight) { - updateMatrix(); - final boolean matrixIsIdentity = mTransformationInfo == null - || mTransformationInfo.mMatrixIsIdentity; + final boolean matrixIsIdentity = hasIdentityMatrix(); if (matrixIsIdentity) { if (mAttachInfo != null) { int maxRight; @@ -10325,17 +10321,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, int height = mBottom - mTop; mRight = right; - if (mDisplayList != null) { - mDisplayList.setRight(mRight); - } + mRenderNode.setRight(mRight); sizeChange(mRight - mLeft, height, oldWidth, height); if (!matrixIsIdentity) { - if ((mPrivateFlags & PFLAG_PIVOT_EXPLICITLY_SET) == 0) { - // A change in dimension means an auto-centered pivot point changes, too - mTransformationInfo.mMatrixDirty = true; - } mPrivateFlags |= PFLAG_DRAWN; // force another invalidation with the new orientation invalidate(true); } @@ -10357,7 +10347,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ @ViewDebug.ExportedProperty(category = "drawing") public float getX() { - return mLeft + (mTransformationInfo != null ? mTransformationInfo.mTranslationX : 0); + return mLeft + getTranslationX(); } /** @@ -10380,7 +10370,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ @ViewDebug.ExportedProperty(category = "drawing") public float getY() { - return mTop + (mTransformationInfo != null ? mTransformationInfo.mTranslationY : 0); + return mTop + getTranslationY(); } /** @@ -10404,7 +10394,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ @ViewDebug.ExportedProperty(category = "drawing") public float getTranslationX() { - return mTransformationInfo != null ? mTransformationInfo.mTranslationX : 0; + return mRenderNode.getTranslationX(); } /** @@ -10418,21 +10408,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @attr ref android.R.styleable#View_translationX */ public void setTranslationX(float translationX) { - ensureTransformationInfo(); - final TransformationInfo info = mTransformationInfo; - if (info.mTranslationX != translationX) { - // Double-invalidation is necessary to capture view's old and new areas + if (translationX != getTranslationX()) { invalidateViewProperty(true, false); - info.mTranslationX = translationX; - info.mMatrixDirty = true; + mRenderNode.setTranslationX(translationX); invalidateViewProperty(false, true); - if (mDisplayList != null) { - mDisplayList.setTranslationX(translationX); - } - if ((mPrivateFlags2 & PFLAG2_VIEW_QUICK_REJECTED) == PFLAG2_VIEW_QUICK_REJECTED) { - // View was rejected last time it was drawn by its parent; this may have changed - invalidateParentIfNeeded(); - } + + invalidateParentIfNeededAndWasQuickRejected(); } } @@ -10446,7 +10427,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ @ViewDebug.ExportedProperty(category = "drawing") public float getTranslationY() { - return mTransformationInfo != null ? mTransformationInfo.mTranslationY : 0; + return mRenderNode.getTranslationY(); } /** @@ -10460,37 +10441,169 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @attr ref android.R.styleable#View_translationY */ public void setTranslationY(float translationY) { - ensureTransformationInfo(); - final TransformationInfo info = mTransformationInfo; - if (info.mTranslationY != translationY) { + if (translationY != getTranslationY()) { invalidateViewProperty(true, false); - info.mTranslationY = translationY; - info.mMatrixDirty = true; + mRenderNode.setTranslationY(translationY); invalidateViewProperty(false, true); - if (mDisplayList != null) { - mDisplayList.setTranslationY(translationY); + + invalidateParentIfNeededAndWasQuickRejected(); + } + } + + /** + * The depth location of this view relative to its parent. + * + * @return The depth of this view relative to its parent. + */ + @ViewDebug.ExportedProperty(category = "drawing") + public float getTranslationZ() { + return mRenderNode.getTranslationZ(); + } + + /** + * Sets the depth location of this view relative to its parent. + * + * @attr ref android.R.styleable#View_translationZ + */ + public void setTranslationZ(float translationZ) { + if (translationZ != getTranslationZ()) { + invalidateViewProperty(true, false); + mRenderNode.setTranslationZ(translationZ); + invalidateViewProperty(false, true); + + invalidateParentIfNeededAndWasQuickRejected(); + } + } + + /** + * Returns a ValueAnimator which can animate a clipping circle. + * <p> + * The View will be clipped to the animating circle. + * <p> + * Any shadow cast by the View will respect the circular clip from this animator. + * + * @param centerX The x coordinate of the center of the animating circle. + * @param centerY The y coordinate of the center of the animating circle. + * @param startRadius The starting radius of the animating circle. + * @param endRadius The ending radius of the animating circle. + */ + public final ValueAnimator createRevealAnimator(int centerX, int centerY, + float startRadius, float endRadius) { + return RevealAnimator.ofRevealCircle(this, centerX, centerY, + startRadius, endRadius, false); + } + + /** + * Returns a ValueAnimator which can animate a clearing circle. + * <p> + * The View is prevented from drawing within the circle, so the content + * behind the View shows through. + * + * @param centerX The x coordinate of the center of the animating circle. + * @param centerY The y coordinate of the center of the animating circle. + * @param startRadius The starting radius of the animating circle. + * @param endRadius The ending radius of the animating circle. + * + * @hide + */ + public final ValueAnimator createClearCircleAnimator(int centerX, int centerY, + float startRadius, float endRadius) { + return RevealAnimator.ofRevealCircle(this, centerX, centerY, + startRadius, endRadius, true); + } + + /** + * Sets the outline of the view, which defines the shape of the shadow it + * casts, and can used for clipping. + * <p> + * If the outline is not set or is null, shadows will be cast from the + * bounds of the View, and clipToOutline will be ignored. + * + * @param outline The new outline of the view. + * Must be {@link android.graphics.Outline#isValid() valid.} + * + * @see #getClipToOutline() + * @see #setClipToOutline(boolean) + */ + public void setOutline(@Nullable Outline outline) { + if (outline != null && !outline.isValid()) { + throw new IllegalArgumentException("Outline must not be invalid"); + } + + mPrivateFlags3 |= PFLAG3_OUTLINE_DEFINED; + + if (outline == null) { + mOutline = null; + } else { + // always copy the path since caller may reuse + if (mOutline == null) { + mOutline = new Outline(); } - if ((mPrivateFlags2 & PFLAG2_VIEW_QUICK_REJECTED) == PFLAG2_VIEW_QUICK_REJECTED) { - // View was rejected last time it was drawn by its parent; this may have changed - invalidateParentIfNeeded(); + mOutline.set(outline); + } + mRenderNode.setOutline(mOutline); + } + + /** + * Returns whether the outline of the View will be used for clipping. + * + * @see #setOutline(Outline) + */ + public final boolean getClipToOutline() { + return ((mPrivateFlags3 & PFLAG3_CLIP_TO_OUTLINE) != 0); + } + + /** + * Sets whether the outline of the View will be used for clipping. + * <p> + * The current implementation of outline clipping uses + * {@link Canvas#clipPath(Path) path clipping}, + * and thus does not support anti-aliasing, and is expensive in terms of + * graphics performance. Therefore, it is strongly recommended that this + * property only be set temporarily, as in an animation. For the same + * reasons, there is no parallel XML attribute for this property. + * <p> + * If the outline of the view is not set or is empty, no clipping will be + * performed. + * + * @see #setOutline(Outline) + */ + public void setClipToOutline(boolean clipToOutline) { + // TODO : Add a fast invalidation here. + if (getClipToOutline() != clipToOutline) { + if (clipToOutline) { + mPrivateFlags3 |= PFLAG3_CLIP_TO_OUTLINE; + } else { + mPrivateFlags3 &= ~PFLAG3_CLIP_TO_OUTLINE; } + mRenderNode.setClipToOutline(clipToOutline); } } /** + * Private API to be used for reveal animation + * + * @hide + */ + public void setRevealClip(boolean shouldClip, boolean inverseClip, + float x, float y, float radius) { + mRenderNode.setRevealClip(shouldClip, inverseClip, x, y, radius); + // TODO: Handle this invalidate in a better way, or purely in native. + invalidate(); + } + + /** * Hit rectangle in parent's coordinates * * @param outRect The hit rectangle of the view. */ public void getHitRect(Rect outRect) { - updateMatrix(); - final TransformationInfo info = mTransformationInfo; - if (info == null || info.mMatrixIsIdentity || mAttachInfo == null) { + if (hasIdentityMatrix() || mAttachInfo == null) { outRect.set(mLeft, mTop, mRight, mBottom); } else { final RectF tmpRect = mAttachInfo.mTmpTransformRect; tmpRect.set(0, 0, getWidth(), getHeight()); - info.mMatrix.mapRect(tmpRect); + getMatrix().mapRect(tmpRect); // TODO: mRenderNode.mapRect(tmpRect) outRect.set((int) tmpRect.left + mLeft, (int) tmpRect.top + mTop, (int) tmpRect.right + mLeft, (int) tmpRect.bottom + mTop); } @@ -10579,11 +10692,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public void offsetTopAndBottom(int offset) { if (offset != 0) { - updateMatrix(); - final boolean matrixIsIdentity = mTransformationInfo == null - || mTransformationInfo.mMatrixIsIdentity; + final boolean matrixIsIdentity = hasIdentityMatrix(); if (matrixIsIdentity) { - if (mDisplayList != null) { + if (isHardwareAccelerated()) { invalidateViewProperty(false, false); } else { final ViewParent p = mParent; @@ -10611,8 +10722,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mTop += offset; mBottom += offset; - if (mDisplayList != null) { - mDisplayList.offsetTopAndBottom(offset); + mRenderNode.offsetTopAndBottom(offset); + if (isHardwareAccelerated()) { invalidateViewProperty(false, false); } else { if (!matrixIsIdentity) { @@ -10630,11 +10741,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public void offsetLeftAndRight(int offset) { if (offset != 0) { - updateMatrix(); - final boolean matrixIsIdentity = mTransformationInfo == null - || mTransformationInfo.mMatrixIsIdentity; + final boolean matrixIsIdentity = hasIdentityMatrix(); if (matrixIsIdentity) { - if (mDisplayList != null) { + if (isHardwareAccelerated()) { invalidateViewProperty(false, false); } else { final ViewParent p = mParent; @@ -10659,8 +10768,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mLeft += offset; mRight += offset; - if (mDisplayList != null) { - mDisplayList.offsetLeftAndRight(offset); + mRenderNode.offsetLeftAndRight(offset); + if (isHardwareAccelerated()) { invalidateViewProperty(false, false); } else { if (!matrixIsIdentity) { @@ -10931,94 +11040,54 @@ public class View implements Drawable.Callback, KeyEvent.Callback, (!(mParent instanceof ViewGroup) || !((ViewGroup) mParent).isViewTransitioning(this)); } + /** * Mark the area defined by dirty as needing to be drawn. If the view is - * visible, {@link #onDraw(android.graphics.Canvas)} will be called at some point - * in the future. This must be called from a UI thread. To call from a non-UI - * thread, call {@link #postInvalidate()}. + * visible, {@link #onDraw(android.graphics.Canvas)} will be called at some + * point in the future. + * <p> + * This must be called from a UI thread. To call from a non-UI thread, call + * {@link #postInvalidate()}. + * <p> + * <b>WARNING:</b> In API 19 and below, this method may be destructive to + * {@code dirty}. * - * WARNING: This method is destructive to dirty. * @param dirty the rectangle representing the bounds of the dirty region */ public void invalidate(Rect dirty) { - if (skipInvalidate()) { - return; - } - if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS) || - (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID || - (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED) { - mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID; - mPrivateFlags |= PFLAG_INVALIDATED; - mPrivateFlags |= PFLAG_DIRTY; - final ViewParent p = mParent; - final AttachInfo ai = mAttachInfo; - //noinspection PointlessBooleanExpression,ConstantConditions - if (!HardwareRenderer.RENDER_DIRTY_REGIONS) { - if (p != null && ai != null && ai.mHardwareAccelerated) { - // fast-track for GL-enabled applications; just invalidate the whole hierarchy - // with a null dirty rect, which tells the ViewAncestor to redraw everything - p.invalidateChild(this, null); - return; - } - } - if (p != null && ai != null) { - final int scrollX = mScrollX; - final int scrollY = mScrollY; - final Rect r = ai.mTmpInvalRect; - r.set(dirty.left - scrollX, dirty.top - scrollY, - dirty.right - scrollX, dirty.bottom - scrollY); - mParent.invalidateChild(this, r); - } - } + final int scrollX = mScrollX; + final int scrollY = mScrollY; + invalidateInternal(dirty.left - scrollX, dirty.top - scrollY, + dirty.right - scrollX, dirty.bottom - scrollY, true, false); } /** - * Mark the area defined by the rect (l,t,r,b) as needing to be drawn. - * The coordinates of the dirty rect are relative to the view. - * If the view is visible, {@link #onDraw(android.graphics.Canvas)} - * will be called at some point in the future. This must be called from - * a UI thread. To call from a non-UI thread, call {@link #postInvalidate()}. + * Mark the area defined by the rect (l,t,r,b) as needing to be drawn. The + * coordinates of the dirty rect are relative to the view. If the view is + * visible, {@link #onDraw(android.graphics.Canvas)} will be called at some + * point in the future. + * <p> + * This must be called from a UI thread. To call from a non-UI thread, call + * {@link #postInvalidate()}. + * * @param l the left position of the dirty region * @param t the top position of the dirty region * @param r the right position of the dirty region * @param b the bottom position of the dirty region */ public void invalidate(int l, int t, int r, int b) { - if (skipInvalidate()) { - return; - } - if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS) || - (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID || - (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED) { - mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID; - mPrivateFlags |= PFLAG_INVALIDATED; - mPrivateFlags |= PFLAG_DIRTY; - final ViewParent p = mParent; - final AttachInfo ai = mAttachInfo; - //noinspection PointlessBooleanExpression,ConstantConditions - if (!HardwareRenderer.RENDER_DIRTY_REGIONS) { - if (p != null && ai != null && ai.mHardwareAccelerated) { - // fast-track for GL-enabled applications; just invalidate the whole hierarchy - // with a null dirty rect, which tells the ViewAncestor to redraw everything - p.invalidateChild(this, null); - return; - } - } - if (p != null && ai != null && l < r && t < b) { - final int scrollX = mScrollX; - final int scrollY = mScrollY; - final Rect tmpr = ai.mTmpInvalRect; - tmpr.set(l - scrollX, t - scrollY, r - scrollX, b - scrollY); - p.invalidateChild(this, tmpr); - } - } + final int scrollX = mScrollX; + final int scrollY = mScrollY; + invalidateInternal(l - scrollX, t - scrollY, r - scrollX, b - scrollY, true, false); } /** * Invalidate the whole view. If the view is visible, * {@link #onDraw(android.graphics.Canvas)} will be called at some point in - * the future. This must be called from a UI thread. To call from a non-UI thread, - * call {@link #postInvalidate()}. + * the future. + * <p> + * This must be called from a UI thread. To call from a non-UI thread, call + * {@link #postInvalidate()}. */ public void invalidate() { invalidate(true); @@ -11026,47 +11095,102 @@ public class View implements Drawable.Callback, KeyEvent.Callback, /** * This is where the invalidate() work actually happens. A full invalidate() - * causes the drawing cache to be invalidated, but this function can be called with - * invalidateCache set to false to skip that invalidation step for cases that do not - * need it (for example, a component that remains at the same dimensions with the same - * content). + * causes the drawing cache to be invalidated, but this function can be + * called with invalidateCache set to false to skip that invalidation step + * for cases that do not need it (for example, a component that remains at + * the same dimensions with the same content). * - * @param invalidateCache Whether the drawing cache for this view should be invalidated as - * well. This is usually true for a full invalidate, but may be set to false if the - * View's contents or dimensions have not changed. + * @param invalidateCache Whether the drawing cache for this view should be + * invalidated as well. This is usually true for a full + * invalidate, but may be set to false if the View's contents or + * dimensions have not changed. */ void invalidate(boolean invalidateCache) { + invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true); + } + + void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache, + boolean fullInvalidate) { if (skipInvalidate()) { return; } - if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS) || - (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID) || - (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED || isOpaque() != mLastIsOpaque) { - mLastIsOpaque = isOpaque(); - mPrivateFlags &= ~PFLAG_DRAWN; + + if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS) + || (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID) + || (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED + || (fullInvalidate && isOpaque() != mLastIsOpaque)) { + if (fullInvalidate) { + mLastIsOpaque = isOpaque(); + mPrivateFlags &= ~PFLAG_DRAWN; + } + mPrivateFlags |= PFLAG_DIRTY; + if (invalidateCache) { mPrivateFlags |= PFLAG_INVALIDATED; mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID; } + + // Propagate the damage rectangle to the parent view. final AttachInfo ai = mAttachInfo; final ViewParent p = mParent; - //noinspection PointlessBooleanExpression,ConstantConditions - if (!HardwareRenderer.RENDER_DIRTY_REGIONS) { - if (p != null && ai != null && ai.mHardwareAccelerated) { - // fast-track for GL-enabled applications; just invalidate the whole hierarchy - // with a null dirty rect, which tells the ViewAncestor to redraw everything - p.invalidateChild(this, null); - return; + if (p != null && ai != null && l < r && t < b) { + final Rect damage = ai.mTmpInvalRect; + damage.set(l, t, r, b); + p.invalidateChild(this, damage); + } + + // Damage the entire projection receiver, if necessary. + if (mBackground != null && mBackground.isProjected()) { + final View receiver = getProjectionReceiver(); + if (receiver != null) { + receiver.damageInParent(); } } - if (p != null && ai != null) { - final Rect r = ai.mTmpInvalRect; - r.set(0, 0, mRight - mLeft, mBottom - mTop); - // Don't call invalidate -- we don't want to internally scroll - // our own bounds - p.invalidateChild(this, r); + // Damage the entire IsolatedZVolume recieving this view's shadow. + if (isHardwareAccelerated() && getTranslationZ() != 0) { + damageShadowReceiver(); + } + } + } + + /** + * @return this view's projection receiver, or {@code null} if none exists + */ + private View getProjectionReceiver() { + ViewParent p = getParent(); + while (p != null && p instanceof View) { + final View v = (View) p; + if (v.isProjectionReceiver()) { + return v; + } + p = p.getParent(); + } + + return null; + } + + /** + * @return whether the view is a projection receiver + */ + private boolean isProjectionReceiver() { + return mBackground != null; + } + + /** + * Damage area of the screen that can be covered by this View's shadow. + * + * This method will guarantee that any changes to shadows cast by a View + * are damaged on the screen for future redraw. + */ + private void damageShadowReceiver() { + final AttachInfo ai = mAttachInfo; + if (ai != null) { + ViewParent p = getParent(); + if (p != null && p instanceof ViewGroup) { + final ViewGroup vg = (ViewGroup) p; + vg.damageInParent(); } } } @@ -11088,7 +11212,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * list properties are not being used in this view */ void invalidateViewProperty(boolean invalidateParent, boolean forceRedraw) { - if (mDisplayList == null || (mPrivateFlags & PFLAG_DRAW_ANIMATION) == PFLAG_DRAW_ANIMATION) { + if (!isHardwareAccelerated() + || !mRenderNode.isValid() + || (mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0) { if (invalidateParent) { invalidateParentCaches(); } @@ -11097,16 +11223,28 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } invalidate(false); } else { - final AttachInfo ai = mAttachInfo; - final ViewParent p = mParent; - if (p != null && ai != null) { - final Rect r = ai.mTmpInvalRect; - r.set(0, 0, mRight - mLeft, mBottom - mTop); - if (mParent instanceof ViewGroup) { - ((ViewGroup) mParent).invalidateChildFast(this, r); - } else { - mParent.invalidateChild(this, r); - } + damageInParent(); + } + if (isHardwareAccelerated() && invalidateParent && getTranslationZ() != 0) { + damageShadowReceiver(); + } + } + + /** + * Tells the parent view to damage this view's bounds. + * + * @hide + */ + protected void damageInParent() { + final AttachInfo ai = mAttachInfo; + final ViewParent p = mParent; + if (p != null && ai != null) { + final Rect r = ai.mTmpInvalRect; + r.set(0, 0, mRight - mLeft, mBottom - mTop); + if (mParent instanceof ViewGroup) { + ((ViewGroup) mParent).damageChild(this, r); + } else { + mParent.invalidateChild(this, r); } } } @@ -11157,6 +11295,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * @hide + */ + protected void invalidateParentIfNeededAndWasQuickRejected() { + if ((mPrivateFlags2 & PFLAG2_VIEW_QUICK_REJECTED) != 0) { + // View was rejected last time it was drawn by its parent; this may have changed + invalidateParentIfNeeded(); + } + } + + /** * Indicates whether this View is opaque. An opaque View guarantees that it will * draw all the pixels overlapping its bounds using a fully opaque color. * @@ -11230,6 +11378,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * @hide + */ + public HardwareRenderer getHardwareRenderer() { + return mAttachInfo != null ? mAttachInfo.mHardwareRenderer : null; + } + + /** * <p>Causes the Runnable to be added to the message queue. * The runnable will be run on the user interface thread.</p> * @@ -11346,10 +11501,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, attachInfo.mHandler.removeCallbacks(action); attachInfo.mViewRootImpl.mChoreographer.removeCallbacks( Choreographer.CALLBACK_ANIMATION, action, null); - } else { - // Assume that post will succeed later - ViewRootImpl.getRunQueue().removeCallbacks(action); } + // Assume that post will succeed later + ViewRootImpl.getRunQueue().removeCallbacks(action); } return true; } @@ -11840,7 +11994,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @attr ref android.R.styleable#View_scrollbarStyle */ - public void setScrollBarStyle(int style) { + public void setScrollBarStyle(@ScrollBarStyle int style) { if (style != (mViewFlags & SCROLLBARS_STYLE_MASK)) { mViewFlags = (mViewFlags & ~SCROLLBARS_STYLE_MASK) | (style & SCROLLBARS_STYLE_MASK); computeOpaqueFlags(); @@ -11864,6 +12018,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @ViewDebug.IntToString(from = SCROLLBARS_OUTSIDE_OVERLAY, to = "OUTSIDE_OVERLAY"), @ViewDebug.IntToString(from = SCROLLBARS_OUTSIDE_INSET, to = "OUTSIDE_INSET") }) + @ScrollBarStyle public int getScrollBarStyle() { return mViewFlags & SCROLLBARS_STYLE_MASK; } @@ -12255,10 +12410,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, InputMethodManager imm = InputMethodManager.peekInstance(); imm.focusIn(this); } - - if (mDisplayList != null) { - mDisplayList.clearDirty(); - } } /** @@ -12365,7 +12516,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see #LAYOUT_DIRECTION_LTR * @see #LAYOUT_DIRECTION_RTL */ - public void onRtlPropertiesChanged(int layoutDirection) { + public void onRtlPropertiesChanged(@ResolvedLayoutDir int layoutDirection) { } /** @@ -12559,6 +12710,19 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see #onAttachedToWindow() */ protected void onDetachedFromWindow() { + } + + /** + * This is a framework-internal mirror of onDetachedFromWindow() that's called + * after onDetachedFromWindow(). + * + * If you override this you *MUST* call super.onDetachedFromWindowInternal()! + * The super method should be called at the end of the overriden method to ensure + * subclasses are destroyed first + * + * @hide + */ + protected void onDetachedFromWindowInternal() { mPrivateFlags &= ~PFLAG_CANCEL_NEXT_UP_EVENT; mPrivateFlags3 &= ~PFLAG3_IS_LAID_OUT; @@ -12576,15 +12740,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } private void cleanupDraw() { + resetDisplayList(); if (mAttachInfo != null) { - if (mDisplayList != null) { - mDisplayList.markDirty(); - mAttachInfo.mViewRootImpl.enqueueDisplayList(mDisplayList); - } mAttachInfo.mViewRootImpl.cancelInvalidate(this); - } else { - // Should never happen - resetDisplayList(); } } @@ -12752,6 +12910,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } onDetachedFromWindow(); + onDetachedFromWindowInternal(); ListenerInfo li = mListenerInfo; final CopyOnWriteArrayList<OnAttachStateChangeListener> listeners = @@ -13053,11 +13212,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } if (layerType == mLayerType) { - if (layerType != LAYER_TYPE_NONE && paint != mLayerPaint) { - mLayerPaint = paint == null ? new Paint() : paint; - invalidateParentCaches(); - invalidate(true); - } + setLayerPaint(paint); return; } @@ -13114,7 +13269,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (layerType == LAYER_TYPE_HARDWARE) { HardwareLayer layer = getHardwareLayer(); if (layer != null) { - layer.setLayerPaint(paint); + layer.setLayerPaint(mLayerPaint); } invalidateViewProperty(false, false); } else { @@ -13174,19 +13329,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback, switch (mLayerType) { case LAYER_TYPE_HARDWARE: - if (attachInfo.mHardwareRenderer != null && - attachInfo.mHardwareRenderer.isEnabled() && - attachInfo.mHardwareRenderer.validate()) { - getHardwareLayer(); - // TODO: We need a better way to handle this case - // If views have registered pre-draw listeners they need - // to be notified before we build the layer. Those listeners - // may however rely on other events to happen first so we - // cannot just invoke them here until they don't cancel the - // current frame - if (!attachInfo.mTreeObserver.hasOnPreDrawListeners()) { - attachInfo.mViewRootImpl.dispatchFlushHardwareLayerUpdates(); - } + getHardwareLayer(); + // TODO: We need a better way to handle this case + // If views have registered pre-draw listeners they need + // to be notified before we build the layer. Those listeners + // may however rely on other events to happen first so we + // cannot just invoke them here until they don't cancel the + // current frame + if (!attachInfo.mTreeObserver.hasOnPreDrawListeners()) { + attachInfo.mViewRootImpl.dispatchFlushHardwareLayerUpdates(); } break; case LAYER_TYPE_SOFTWARE: @@ -13207,8 +13358,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return null; } - if (!mAttachInfo.mHardwareRenderer.validate()) return null; - final int width = mRight - mLeft; final int height = mBottom - mTop; @@ -13218,16 +13367,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0 || mHardwareLayer == null) { if (mHardwareLayer == null) { - mHardwareLayer = mAttachInfo.mHardwareRenderer.createHardwareLayer( - width, height, isOpaque()); + mHardwareLayer = mAttachInfo.mHardwareRenderer.createDisplayListLayer( + width, height); mLocalDirtyRect.set(0, 0, width, height); - } else { - if (mHardwareLayer.getWidth() != width || mHardwareLayer.getHeight() != height) { - if (mHardwareLayer.resize(width, height)) { - mLocalDirtyRect.set(0, 0, width, height); - } - } - + } else if (mHardwareLayer.isValid()) { // This should not be necessary but applications that change // the parameters of their background drawable without calling // this.setBackground(Drawable) can leave the view in a bad state @@ -13235,23 +13378,21 @@ public class View implements Drawable.Callback, KeyEvent.Callback, // not opaque.) computeOpaqueFlags(); - final boolean opaque = isOpaque(); - if (mHardwareLayer.isValid() && mHardwareLayer.isOpaque() != opaque) { - mHardwareLayer.setOpaque(opaque); + if (mHardwareLayer.prepare(width, height, isOpaque())) { mLocalDirtyRect.set(0, 0, width, height); } } // The layer is not valid if the underlying GPU resources cannot be allocated + mHardwareLayer.flushChanges(); if (!mHardwareLayer.isValid()) { return null; } mHardwareLayer.setLayerPaint(mLayerPaint); - mHardwareLayer.redrawLater(getHardwareLayerDisplayList(mHardwareLayer), mLocalDirtyRect); - ViewRootImpl viewRoot = getViewRootImpl(); - if (viewRoot != null) viewRoot.pushHardwareLayerUpdate(mHardwareLayer); - + RenderNode displayList = mHardwareLayer.startRecording(); + updateDisplayListIfDirty(displayList, true); + mHardwareLayer.endRecording(mLocalDirtyRect); mLocalDirtyRect.setEmpty(); } @@ -13268,18 +13409,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ boolean destroyLayer(boolean valid) { if (mHardwareLayer != null) { - AttachInfo info = mAttachInfo; - if (info != null && info.mHardwareRenderer != null && - info.mHardwareRenderer.isEnabled() && - (valid || info.mHardwareRenderer.validate())) { - - info.mHardwareRenderer.cancelLayerUpdate(mHardwareLayer); - mHardwareLayer.destroy(); - mHardwareLayer = null; + mHardwareLayer.destroy(); + mHardwareLayer = null; - invalidate(true); - invalidateParentCaches(); - } + invalidate(true); + invalidateParentCaches(); return true; } return false; @@ -13394,46 +13528,37 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** - * @return The {@link HardwareRenderer} associated with that view or null if - * hardware rendering is not supported or this view is not attached - * to a window. - * - * @hide - */ - public HardwareRenderer getHardwareRenderer() { - if (mAttachInfo != null) { - return mAttachInfo.mHardwareRenderer; - } - return null; - } - - /** * Returns a DisplayList. If the incoming displayList is null, one will be created. * Otherwise, the same display list will be returned (after having been rendered into * along the way, depending on the invalidation state of the view). * - * @param displayList The previous version of this displayList, could be null. + * @param renderNode The previous version of this displayList, could be null. * @param isLayer Whether the requester of the display list is a layer. If so, * the view will avoid creating a layer inside the resulting display list. * @return A new or reused DisplayList object. */ - private DisplayList getDisplayList(DisplayList displayList, boolean isLayer) { + private void updateDisplayListIfDirty(@NonNull RenderNode renderNode, boolean isLayer) { + if (renderNode == null) { + throw new IllegalArgumentException("RenderNode must not be null"); + } if (!canHaveDisplayList()) { - return null; + // can't populate RenderNode, don't try + return; } - if (((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0 || - displayList == null || !displayList.isValid() || - (!isLayer && mRecreateDisplayList))) { + if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0 + || !renderNode.isValid() + || (!isLayer && mRecreateDisplayList)) { // Don't need to recreate the display list, just need to tell our // children to restore/recreate theirs - if (displayList != null && displayList.isValid() && - !isLayer && !mRecreateDisplayList) { + if (renderNode.isValid() + && !isLayer + && !mRecreateDisplayList) { mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID; mPrivateFlags &= ~PFLAG_DIRTY_MASK; dispatchGetDisplayList(); - return displayList; + return; // no work needed } if (!isLayer) { @@ -13441,20 +13566,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback, // we copy in child display lists into ours in drawChild() mRecreateDisplayList = true; } - if (displayList == null) { - displayList = mAttachInfo.mHardwareRenderer.createDisplayList(getClass().getName()); - // If we're creating a new display list, make sure our parent gets invalidated - // since they will need to recreate their display list to account for this - // new child display list. - invalidateParentCaches(); - } boolean caching = false; int width = mRight - mLeft; int height = mBottom - mTop; int layerType = getLayerType(); - final HardwareCanvas canvas = displayList.start(width, height); + final HardwareCanvas canvas = renderNode.start(width, height); try { if (!isLayer && layerType != LAYER_TYPE_NONE) { @@ -13497,57 +13615,40 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } } } finally { - displayList.end(); - displayList.setCaching(caching); + renderNode.end(canvas); + renderNode.setCaching(caching); if (isLayer) { - displayList.setLeftTopRightBottom(0, 0, width, height); + renderNode.setLeftTopRightBottom(0, 0, width, height); } else { - setDisplayListProperties(displayList); + setDisplayListProperties(renderNode); } } } else if (!isLayer) { mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID; mPrivateFlags &= ~PFLAG_DIRTY_MASK; } - - return displayList; } /** - * Get the DisplayList for the HardwareLayer + * Returns a RenderNode with View draw content recorded, which can be + * used to draw this view again without executing its draw method. * - * @param layer The HardwareLayer whose DisplayList we want - * @return A DisplayList fopr the specified HardwareLayer - */ - private DisplayList getHardwareLayerDisplayList(HardwareLayer layer) { - DisplayList displayList = getDisplayList(layer.getDisplayList(), true); - layer.setDisplayList(displayList); - return displayList; - } - - - /** - * <p>Returns a display list that can be used to draw this view again - * without executing its draw method.</p> - * - * @return A DisplayList ready to replay, or null if caching is not enabled. + * @return A RenderNode ready to replay, or null if caching is not enabled. * * @hide */ - public DisplayList getDisplayList() { - mDisplayList = getDisplayList(mDisplayList, false); - return mDisplayList; + public RenderNode getDisplayList() { + updateDisplayListIfDirty(mRenderNode, false); + return mRenderNode; } - private void clearDisplayList() { - if (mDisplayList != null) { - mDisplayList.clear(); + private void resetDisplayList() { + if (mRenderNode.isValid()) { + mRenderNode.destroyDisplayListData(); } - } - private void resetDisplayList() { - if (mDisplayList != null) { - mDisplayList.reset(); + if (mBackgroundDisplayList != null && mBackgroundDisplayList.isValid()) { + mBackgroundDisplayList.destroyDisplayListData(); } } @@ -14137,17 +14238,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** - * This method is called by getDisplayList() when a display list is created or re-rendered. - * It sets or resets the current value of all properties on that display list (resetting is - * necessary when a display list is being re-created, because we need to make sure that - * previously-set transform values + * This method is called by getDisplayList() when a display list is recorded for a View. + * It pushes any properties to the RenderNode that aren't managed by the RenderNode. */ - void setDisplayListProperties(DisplayList displayList) { - if (displayList != null) { - displayList.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom); - displayList.setHasOverlappingRendering(hasOverlappingRendering()); + void setDisplayListProperties(RenderNode renderNode) { + if (renderNode != null) { + renderNode.setHasOverlappingRendering(hasOverlappingRendering()); if (mParent instanceof ViewGroup) { - displayList.setClipToBounds( + renderNode.setClipToBounds( (((ViewGroup) mParent).mGroupFlags & ViewGroup.FLAG_CLIP_CHILDREN) != 0); } float alpha = 1; @@ -14162,7 +14260,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, alpha = t.getAlpha(); } if ((transformType & Transformation.TYPE_MATRIX) != 0) { - displayList.setMatrix(t.getMatrix()); + renderNode.setStaticMatrix(t.getMatrix()); } } } @@ -14175,22 +14273,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, alpha = 1; } } - displayList.setTransformationInfo(alpha, - mTransformationInfo.mTranslationX, mTransformationInfo.mTranslationY, - mTransformationInfo.mRotation, mTransformationInfo.mRotationX, - mTransformationInfo.mRotationY, mTransformationInfo.mScaleX, - mTransformationInfo.mScaleY); - if (mTransformationInfo.mCamera == null) { - mTransformationInfo.mCamera = new Camera(); - mTransformationInfo.matrix3D = new Matrix(); - } - displayList.setCameraDistance(mTransformationInfo.mCamera.getLocationZ()); - if ((mPrivateFlags & PFLAG_PIVOT_EXPLICITLY_SET) == PFLAG_PIVOT_EXPLICITLY_SET) { - displayList.setPivotX(getPivotX()); - displayList.setPivotY(getPivotY()); - } + renderNode.setAlpha(alpha); } else if (alpha < 1) { - displayList.setAlpha(alpha); + renderNode.setAlpha(alpha); } } } @@ -14237,10 +14322,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } transformToApply = parent.getChildTransformation(); } else { - if ((mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_TRANSFORM) == - PFLAG3_VIEW_IS_ANIMATING_TRANSFORM && mDisplayList != null) { + if ((mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_TRANSFORM) != 0) { // No longer animating: clear out old animation matrix - mDisplayList.setAnimationMatrix(null); + mRenderNode.setAnimationMatrix(null); mPrivateFlags3 &= ~PFLAG3_VIEW_IS_ANIMATING_TRANSFORM; } if (!useDisplayListProperties && @@ -14278,7 +14362,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mPrivateFlags &= ~PFLAG_INVALIDATED; } - DisplayList displayList = null; + RenderNode displayList = null; Bitmap cache = null; boolean hasDisplayList = false; if (caching) { @@ -14570,24 +14654,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, int saveCount; if (!dirtyOpaque) { - final Drawable background = mBackground; - if (background != null) { - final int scrollX = mScrollX; - final int scrollY = mScrollY; - - if (mBackgroundSizeChanged) { - background.setBounds(0, 0, mRight - mLeft, mBottom - mTop); - mBackgroundSizeChanged = false; - } - - if ((scrollX | scrollY) == 0) { - background.draw(canvas); - } else { - canvas.translate(scrollX, scrollY); - background.draw(canvas); - canvas.translate(-scrollX, -scrollY); - } - } + drawBackground(canvas); } // skip step 2 & 5 if possible (common case) @@ -14754,6 +14821,89 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * Draws the background onto the specified canvas. + * + * @param canvas Canvas on which to draw the background + */ + private void drawBackground(Canvas canvas) { + final Drawable background = mBackground; + if (background == null) { + return; + } + + if (mBackgroundSizeChanged) { + background.setBounds(0, 0, mRight - mLeft, mBottom - mTop); + mBackgroundSizeChanged = false; + if ((mPrivateFlags3 & PFLAG3_OUTLINE_DEFINED) == 0) { + // Outline not currently define, query from background + mOutline = background.getOutline(); + mRenderNode.setOutline(mOutline); + } + } + + // Attempt to use a display list if requested. + if (canvas.isHardwareAccelerated() && mAttachInfo != null + && mAttachInfo.mHardwareRenderer != null) { + mBackgroundDisplayList = getDrawableDisplayList(background, mBackgroundDisplayList); + + final RenderNode displayList = mBackgroundDisplayList; + if (displayList != null && displayList.isValid()) { + setBackgroundDisplayListProperties(displayList); + ((HardwareCanvas) canvas).drawDisplayList(displayList); + return; + } + } + + final int scrollX = mScrollX; + final int scrollY = mScrollY; + if ((scrollX | scrollY) == 0) { + background.draw(canvas); + } else { + canvas.translate(scrollX, scrollY); + background.draw(canvas); + canvas.translate(-scrollX, -scrollY); + } + } + + /** + * Set up background drawable display list properties. + * + * @param displayList Valid display list for the background drawable + */ + private void setBackgroundDisplayListProperties(RenderNode displayList) { + displayList.setTranslationX(mScrollX); + displayList.setTranslationY(mScrollY); + } + + /** + * Creates a new display list or updates the existing display list for the + * specified Drawable. + * + * @param drawable Drawable for which to create a display list + * @param displayList Existing display list, or {@code null} + * @return A valid display list for the specified drawable + */ + private RenderNode getDrawableDisplayList(Drawable drawable, RenderNode displayList) { + if (displayList == null) { + displayList = RenderNode.create(drawable.getClass().getName()); + } + + final Rect bounds = drawable.getBounds(); + final int width = bounds.width(); + final int height = bounds.height(); + final HardwareCanvas canvas = displayList.start(width, height); + drawable.draw(canvas); + displayList.end(canvas); + + // Set up drawable properties that are view-independent. + displayList.setLeftTopRightBottom(bounds.left, bounds.top, bounds.right, bounds.bottom); + displayList.setProjectBackwards(drawable.isProjected()); + displayList.setProjectionReceiver(true); + displayList.setClipToBounds(false); + return displayList; + } + + /** * Returns the overlay for this view, creating it if it does not yet exist. * Adding drawables to the overlay will cause them to be displayed whenever * the view itself is redrawn. Objects in the overlay should be actively @@ -15022,20 +15172,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mTop = top; mRight = right; mBottom = bottom; - if (mDisplayList != null) { - mDisplayList.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom); - } + mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom); mPrivateFlags |= PFLAG_HAS_BOUNDS; if (sizeChanged) { - if ((mPrivateFlags & PFLAG_PIVOT_EXPLICITLY_SET) == 0) { - // A change in dimension means an auto-centered pivot point changes, too - if (mTransformationInfo != null) { - mTransformationInfo.mMatrixDirty = true; - } - } sizeChange(newWidth, newHeight, oldWidth, oldHeight); } @@ -15094,9 +15236,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @param drawable the drawable to invalidate */ + @Override public void invalidateDrawable(Drawable drawable) { if (verifyDrawable(drawable)) { - final Rect dirty = drawable.getBounds(); + final Rect dirty = drawable.getDirtyBounds(); final int scrollX = mScrollX; final int scrollY = mScrollY; @@ -15113,6 +15256,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @param when the time at which the action must occur. Uses the * {@link SystemClock#uptimeMillis} timebase. */ + @Override public void scheduleDrawable(Drawable who, Runnable what, long when) { if (verifyDrawable(who) && what != null) { final long delay = when - SystemClock.uptimeMillis(); @@ -15132,14 +15276,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @param who the recipient of the action * @param what the action to cancel */ + @Override public void unscheduleDrawable(Drawable who, Runnable what) { if (verifyDrawable(who) && what != null) { if (mAttachInfo != null) { mAttachInfo.mViewRootImpl.mChoreographer.removeCallbacks( Choreographer.CALLBACK_ANIMATION, what, who); - } else { - ViewRootImpl.getRunQueue().removeCallbacks(what); } + ViewRootImpl.getRunQueue().removeCallbacks(what); } } @@ -15202,7 +15346,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @hide */ - public void onResolveDrawables(int layoutDirection) { + public void onResolveDrawables(@ResolvedLayoutDir int layoutDirection) { } /** @@ -15249,7 +15393,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @see Drawable#setState(int[]) */ protected void drawableStateChanged() { - Drawable d = mBackground; + final Drawable d = mBackground; if (d != null && d.isStateful()) { d.setState(getDrawableState()); } @@ -15434,7 +15578,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Drawable d= null; if (resid != 0) { - d = mResources.getDrawable(resid); + d = mContext.getDrawable(resid); } setBackground(d); @@ -15969,8 +16113,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return false; } - transformMotionEventToGlobal(ev); - ev.offsetLocation(info.mWindowLeft, info.mWindowTop); + final Matrix m = info.mTmpMatrix; + m.set(Matrix.IDENTITY_MATRIX); + transformMatrixToGlobal(m); + ev.transform(m); return true; } @@ -15988,54 +16134,60 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return false; } - ev.offsetLocation(-info.mWindowLeft, -info.mWindowTop); - transformMotionEventToLocal(ev); + final Matrix m = info.mTmpMatrix; + m.set(Matrix.IDENTITY_MATRIX); + transformMatrixToLocal(m); + ev.transform(m); return true; } /** - * Recursive helper method that applies transformations in post-order. + * Modifies the input matrix such that it maps view-local coordinates to + * on-screen coordinates. * - * @param ev the on-screen motion event + * @param m input matrix to modify */ - private void transformMotionEventToLocal(MotionEvent ev) { + void transformMatrixToGlobal(Matrix m) { final ViewParent parent = mParent; if (parent instanceof View) { final View vp = (View) parent; - vp.transformMotionEventToLocal(ev); - ev.offsetLocation(vp.mScrollX, vp.mScrollY); + vp.transformMatrixToGlobal(m); + m.postTranslate(-vp.mScrollX, -vp.mScrollY); } else if (parent instanceof ViewRootImpl) { final ViewRootImpl vr = (ViewRootImpl) parent; - ev.offsetLocation(0, vr.mCurScrollY); + vr.transformMatrixToGlobal(m); + m.postTranslate(0, -vr.mCurScrollY); } - ev.offsetLocation(-mLeft, -mTop); + m.postTranslate(mLeft, mTop); if (!hasIdentityMatrix()) { - ev.transform(getInverseMatrix()); + m.postConcat(getMatrix()); } } /** - * Recursive helper method that applies transformations in pre-order. + * Modifies the input matrix such that it maps on-screen coordinates to + * view-local coordinates. * - * @param ev the on-screen motion event + * @param m input matrix to modify */ - private void transformMotionEventToGlobal(MotionEvent ev) { - if (!hasIdentityMatrix()) { - ev.transform(getMatrix()); - } - - ev.offsetLocation(mLeft, mTop); - + void transformMatrixToLocal(Matrix m) { final ViewParent parent = mParent; if (parent instanceof View) { final View vp = (View) parent; - ev.offsetLocation(-vp.mScrollX, -vp.mScrollY); - vp.transformMotionEventToGlobal(ev); + vp.transformMatrixToLocal(m); + m.preTranslate(vp.mScrollX, vp.mScrollY); } else if (parent instanceof ViewRootImpl) { final ViewRootImpl vr = (ViewRootImpl) parent; - ev.offsetLocation(0, -vr.mCurScrollY); + vr.transformMatrixToLocal(m); + m.preTranslate(0, vr.mCurScrollY); + } + + m.preTranslate(-mLeft, -mTop); + + if (!hasIdentityMatrix()) { + m.preConcat(getInverseMatrix()); } } @@ -16318,7 +16470,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, /** * Returns this view's tag. * - * @return the Object stored in this view as a tag + * @return the Object stored in this view as a tag, or {@code null} if not + * set * * @see #setTag(Object) * @see #getTag(int) @@ -16348,7 +16501,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @param key The key identifying the tag * - * @return the Object stored in this view as a tag + * @return the Object stored in this view as a tag, or {@code null} if not + * set * * @see #setTag(int, Object) * @see #getTag() @@ -18020,6 +18174,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @ViewDebug.IntToString(from = TEXT_ALIGNMENT_VIEW_START, to = "VIEW_START"), @ViewDebug.IntToString(from = TEXT_ALIGNMENT_VIEW_END, to = "VIEW_END") }) + @TextAlignment public int getRawTextAlignment() { return (mPrivateFlags2 & PFLAG2_TEXT_ALIGNMENT_MASK) >> PFLAG2_TEXT_ALIGNMENT_MASK_SHIFT; } @@ -18043,7 +18198,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @attr ref android.R.styleable#View_textAlignment */ - public void setTextAlignment(int textAlignment) { + public void setTextAlignment(@TextAlignment int textAlignment) { if (textAlignment != getRawTextAlignment()) { // Reset the current and resolved text alignment mPrivateFlags2 &= ~PFLAG2_TEXT_ALIGNMENT_MASK; @@ -18084,6 +18239,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @ViewDebug.IntToString(from = TEXT_ALIGNMENT_VIEW_START, to = "VIEW_START"), @ViewDebug.IntToString(from = TEXT_ALIGNMENT_VIEW_END, to = "VIEW_END") }) + @TextAlignment public int getTextAlignment() { return (mPrivateFlags2 & PFLAG2_TEXT_ALIGNMENT_RESOLVED_MASK) >> PFLAG2_TEXT_ALIGNMENT_RESOLVED_MASK_SHIFT; @@ -18246,6 +18402,33 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } } + /** + * Gets the Views in the hierarchy affected by entering and exiting Activity Scene transitions. + * @param transitioningViews This View will be added to transitioningViews if it is VISIBLE and + * a normal View or a ViewGroup with + * {@link android.view.ViewGroup#isTransitionGroup()} true. + * @hide + */ + public void captureTransitioningViews(List<View> transitioningViews) { + if (getVisibility() == View.VISIBLE) { + transitioningViews.add(this); + } + } + + /** + * Adds all Views that have {@link #getSharedElementName()} non-null to sharedElements. + * @param sharedElements Will contain all Views in the hierarchy having a shared element name. + * @hide + */ + public void findSharedElements(Map<String, View> sharedElements) { + if (getVisibility() == VISIBLE) { + String sharedElementName = getSharedElementName(); + if (sharedElementName != null) { + sharedElements.put(sharedElementName, this); + } + } + } + // // Properties // @@ -18298,6 +18481,22 @@ public class View implements Drawable.Callback, KeyEvent.Callback, }; /** + * A Property wrapper around the <code>translationZ</code> functionality handled by the + * {@link View#setTranslationZ(float)} and {@link View#getTranslationZ()} methods. + */ + public static final Property<View, Float> TRANSLATION_Z = new FloatProperty<View>("translationZ") { + @Override + public void setValue(View object, float value) { + object.setTranslationZ(value); + } + + @Override + public Float get(View object) { + return object.getTranslationZ(); + } + }; + + /** * A Property wrapper around the <code>x</code> functionality handled by the * {@link View#setX(float)} and {@link View#getX()} methods. */ @@ -18552,10 +18751,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } } - class CheckForLongPress implements Runnable { - + private final class CheckForLongPress implements Runnable { private int mOriginalWindowAttachCount; + @Override public void run() { if (isPressed() && (mParent != null) && mOriginalWindowAttachCount == mWindowAttachCount) { @@ -18571,14 +18770,20 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } private final class CheckForTap implements Runnable { + public float x; + public float y; + + @Override public void run() { mPrivateFlags &= ~PFLAG_PREPRESSED; + setHotspot(R.attr.state_pressed, x, y); setPressed(true); checkForLongClick(ViewConfiguration.getTapTimeout()); } } private final class PerformClick implements Runnable { + @Override public void run() { performClick(); } @@ -18603,6 +18808,35 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * Specifies that the shared name of the View to be shared with another Activity. + * When transitioning between Activities, the name links a UI element in the starting + * Activity to UI element in the called Activity. Names should be unique in the + * View hierarchy. + * + * @param sharedElementName The cross-Activity View identifier. The called Activity will use + * the name to match the location with a View in its layout. + * @see android.app.ActivityOptions#makeSceneTransitionAnimation(android.os.Bundle) + */ + public void setSharedElementName(String sharedElementName) { + setTagInternal(com.android.internal.R.id.shared_element_name, sharedElementName); + } + + /** + * Returns the shared name of the View to be shared with another Activity. + * When transitioning between Activities, the name links a UI element in the starting + * Activity to UI element in the called Activity. Names should be unique in the + * View hierarchy. + * + * <p>This returns null if the View is not a shared element or the name if it is.</p> + * + * @return The name used for this View for cross-Activity transitions or null if + * this View has not been identified as shared. + */ + public String getSharedElementName() { + return (String) getTag(com.android.internal.R.id.shared_element_name); + } + + /** * Interface definition for a callback to be invoked when a hardware key event is * dispatched to this view. The callback will be invoked before the key event is * given to the view. This is only useful for hardware keyboards; a software input @@ -18828,7 +19062,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } private final class UnsetPressedState implements Runnable { + @Override public void run() { + clearHotspot(R.attr.state_pressed); setPressed(false); } } @@ -18921,8 +19157,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, final Callbacks mRootCallbacks; - HardwareCanvas mHardwareCanvas; - IWindowId mIWindowId; WindowId mWindowId; @@ -18932,7 +19166,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, View mRootView; IBinder mPanelParentWindowToken; - Surface mSurface; boolean mHardwareAccelerated; boolean mHardwareAccelerationRequested; @@ -19177,7 +19410,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, /** * The id of the window for accessibility purposes. */ - int mAccessibilityWindowId = View.NO_ID; + int mAccessibilityWindowId = AccessibilityNodeInfo.UNDEFINED_ITEM_ID; /** * Flags related to accessibility processing. diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java index e67659c..5112b9a 100644 --- a/core/java/android/view/ViewConfiguration.java +++ b/core/java/android/view/ViewConfiguration.java @@ -80,7 +80,7 @@ public class ViewConfiguration { * is a tap or a scroll. If the user does not move within this interval, it is * considered to be a tap. */ - private static final int TAP_TIMEOUT = 180; + private static final int TAP_TIMEOUT = 100; /** * Defines the duration in milliseconds we will wait to see if a touch event diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index f346ee8..d2c6302 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -38,7 +38,6 @@ import android.util.Log; import android.util.Pools.SynchronizedPool; import android.util.SparseArray; import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.Animation; import android.view.animation.AnimationUtils; @@ -51,6 +50,8 @@ import com.android.internal.util.Predicate; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; +import java.util.List; +import java.util.Map; import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; @@ -355,6 +356,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager */ private static final int FLAG_LAYOUT_MODE_WAS_EXPLICITLY_SET = 0x800000; + static final int FLAG_IS_TRANSITION_GROUP = 0x1000000; + + static final int FLAG_IS_TRANSITION_GROUP_SET = 0x2000000; + /** * Indicates which types of drawing caches are to be kept in memory. * This field should be made private, so it is hidden from the SDK. @@ -456,20 +461,21 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager private int mChildCountWithTransientState = 0; public ViewGroup(Context context) { - super(context); - initViewGroup(); + this(context, null); } public ViewGroup(Context context, AttributeSet attrs) { - super(context, attrs); - initViewGroup(); - initFromAttributes(context, attrs, 0); + this(context, attrs, 0); } - public ViewGroup(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public ViewGroup(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ViewGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); initViewGroup(); - initFromAttributes(context, attrs, defStyle); + initFromAttributes(context, attrs, defStyleAttr, defStyleRes); } private boolean debugDraw() { @@ -499,8 +505,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager mPersistentDrawingCache = PERSISTENT_SCROLLING_CACHE; } - private void initFromAttributes(Context context, AttributeSet attrs, int defStyle) { - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ViewGroup, defStyle, 0); + private void initFromAttributes( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ViewGroup, defStyleAttr, + defStyleRes); final int N = a.getIndexCount(); for (int i = 0; i < N; i++) { @@ -545,6 +553,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager case R.styleable.ViewGroup_layoutMode: setLayoutMode(a.getInt(attr, LAYOUT_MODE_UNDEFINED)); break; + case R.styleable.ViewGroup_transitionGroup: + setTransitionGroup(a.getBoolean(attr, false)); + break; } } @@ -597,7 +608,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager @Override void handleFocusGainInternal(int direction, Rect previouslyFocusedRect) { if (mFocused != null) { - mFocused.unFocus(); + mFocused.unFocus(this); mFocused = null; } super.handleFocusGainInternal(direction, previouslyFocusedRect); @@ -615,12 +626,12 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } // Unfocus us, if necessary - super.unFocus(); + super.unFocus(focused); // We had a previous notion of who had focus. Clear it. if (mFocused != child) { if (mFocused != null) { - mFocused.unFocus(); + mFocused.unFocus(focused); } mFocused = child; @@ -811,14 +822,14 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * {@inheritDoc} */ @Override - void unFocus() { + void unFocus(View focused) { if (DBG) { System.out.println(this + " unFocus()"); } if (mFocused == null) { - super.unFocus(); + super.unFocus(focused); } else { - mFocused.unFocus(); + mFocused.unFocus(focused); mFocused = null; } } @@ -2277,6 +2288,39 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } /** + * Returns true if this ViewGroup should be considered as a single entity for removal + * when executing an Activity transition. If this is false, child elements will move + * individually during the transition. + * @return True if the ViewGroup should be acted on together during an Activity transition. + * The default value is false when the background is null and true when the background + * is not null or if {@link #getSharedElementName()} is not null. + */ + public boolean isTransitionGroup() { + if ((mGroupFlags & FLAG_IS_TRANSITION_GROUP_SET) != 0) { + return ((mGroupFlags & FLAG_IS_TRANSITION_GROUP) != 0); + } else { + return getBackground() != null || getSharedElementName() != null; + } + } + + /** + * Changes whether or not this ViewGroup should be treated as a single entity during + * ActivityTransitions. + * @param isTransitionGroup Whether or not the ViewGroup should be treated as a unit + * in Activity transitions. If false, the ViewGroup won't transition, + * only its children. If true, the entire ViewGroup will transition + * together. + */ + public void setTransitionGroup(boolean isTransitionGroup) { + mGroupFlags |= FLAG_IS_TRANSITION_GROUP_SET; + if (isTransitionGroup) { + mGroupFlags |= FLAG_IS_TRANSITION_GROUP; + } else { + mGroupFlags &= ~FLAG_IS_TRANSITION_GROUP; + } + } + + /** * {@inheritDoc} */ public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { @@ -2509,13 +2553,13 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfoInternal(info); if (mAttachInfo != null) { - ArrayList<View> childrenForAccessibility = mAttachInfo.mTempArrayList; + final ArrayList<View> childrenForAccessibility = mAttachInfo.mTempArrayList; childrenForAccessibility.clear(); addChildrenForAccessibility(childrenForAccessibility); final int childrenForAccessibilityCount = childrenForAccessibility.size(); for (int i = 0; i < childrenForAccessibilityCount; i++) { - View child = childrenForAccessibility.get(i); - info.addChild(child); + final View child = childrenForAccessibility.get(i); + info.addChildUnchecked(child); } childrenForAccessibility.clear(); } @@ -2583,6 +2627,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager for (int i = 0; i < count; i++) { children[i].dispatchDetachedFromWindow(); } + clearDisappearingChildren(); super.dispatchDetachedFromWindow(); } @@ -3103,7 +3148,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } /** - * Returns whether ths group's children are clipped to their bounds before drawing. + * Returns whether this group's children are clipped to their bounds before drawing. * The default value is true. * @see #setClipChildren(boolean) * @@ -3128,8 +3173,8 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager setBooleanFlag(FLAG_CLIP_CHILDREN, clipChildren); for (int i = 0; i < mChildrenCount; ++i) { View child = getChildAt(i); - if (child.mDisplayList != null) { - child.mDisplayList.setClipToBounds(clipChildren); + if (child.mRenderNode != null) { + child.mRenderNode.setClipToBounds(clipChildren); } } } @@ -3618,7 +3663,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager childHasTransientStateChanged(child, true); } - if (child.isImportantForAccessibility() && child.getVisibility() != View.GONE) { + if (child.getVisibility() != View.GONE) { notifySubtreeAccessibilityStateChangedIfNeeded(); } } @@ -3826,7 +3871,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager boolean clearChildFocus = false; if (view == mFocused) { - view.unFocus(); + view.unFocus(null); clearChildFocus = true; } @@ -3861,7 +3906,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager onViewRemoved(view); - if (view.isImportantForAccessibility() && view.getVisibility() != View.GONE) { + if (view.getVisibility() != View.GONE) { notifySubtreeAccessibilityStateChangedIfNeeded(); } } @@ -3921,7 +3966,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } if (view == focused) { - view.unFocus(); + view.unFocus(null); clearChildFocus = true; } @@ -4008,7 +4053,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } if (view == focused) { - view.unFocus(); + view.unFocus(null); clearChildFocus = true; } @@ -4404,7 +4449,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * * @hide */ - public void invalidateChildFast(View child, final Rect dirty) { + public void damageChild(View child, final Rect dirty) { ViewParent parent = this; final AttachInfo attachInfo = mAttachInfo; @@ -4427,7 +4472,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager parentVG.invalidate(); parent = null; } else { - parent = parentVG.invalidateChildInParentFast(left, top, dirty); + parent = parentVG.damageChildInParent(left, top, dirty); left = parentVG.mLeft; top = parentVG.mTop; } @@ -4449,9 +4494,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * * @hide */ - protected ViewParent invalidateChildInParentFast(int left, int top, final Rect dirty) { - if ((mPrivateFlags & PFLAG_DRAWN) == PFLAG_DRAWN || - (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID) { + protected ViewParent damageChildInParent(int left, int top, final Rect dirty) { + if ((mPrivateFlags & PFLAG_DRAWN) != 0 + || (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) != 0) { dirty.offset(left - mScrollX, top - mScrollY); if ((mGroupFlags & FLAG_CLIP_CHILDREN) == 0) { dirty.union(0, 0, mRight - mLeft, mBottom - mTop); @@ -4564,9 +4609,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final View v = children[i]; v.mTop += offset; v.mBottom += offset; - if (v.mDisplayList != null) { + if (v.mRenderNode != null) { invalidate = true; - v.mDisplayList.offsetTopAndBottom(offset); + v.mRenderNode.offsetTopAndBottom(offset); } } @@ -5217,8 +5262,17 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * this if you don't want animations for exiting views to stack up. */ public void clearDisappearingChildren() { - if (mDisappearingChildren != null) { - mDisappearingChildren.clear(); + final ArrayList<View> disappearingChildren = mDisappearingChildren; + if (disappearingChildren != null) { + final int count = disappearingChildren.size(); + for (int i = 0; i < count; i++) { + final View view = disappearingChildren.get(i); + if (view.mAttachInfo != null) { + view.dispatchDetachedFromWindow(); + } + view.clearAnimation(); + } + disappearingChildren.clear(); invalidate(); } } @@ -5794,6 +5848,37 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager protected void onSetLayoutParams(View child, LayoutParams layoutParams) { } + /** @hide */ + @Override + public void captureTransitioningViews(List<View> transitioningViews) { + if (getVisibility() != View.VISIBLE) { + return; + } + if (isTransitionGroup()) { + transitioningViews.add(this); + } else { + int count = getChildCount(); + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + child.captureTransitioningViews(transitioningViews); + } + } + } + + /** @hide */ + @Override + public void findSharedElements(Map<String, View> sharedElements) { + if (getVisibility() != VISIBLE) { + return; + } + super.findSharedElements(sharedElements); + int count = getChildCount(); + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + child.findSharedElements(sharedElements); + } + } + /** * LayoutParams are used by views to tell their parents how they want to be * laid out. See diff --git a/core/java/android/view/ViewOverlay.java b/core/java/android/view/ViewOverlay.java index 975931a..0cf9ddd 100644 --- a/core/java/android/view/ViewOverlay.java +++ b/core/java/android/view/ViewOverlay.java @@ -155,6 +155,11 @@ public class ViewOverlay { } } + @Override + protected boolean verifyDrawable(Drawable who) { + return super.verifyDrawable(who) || (mDrawables != null && mDrawables.contains(who)); + } + public void add(View child) { if (child.getParent() instanceof ViewGroup) { ViewGroup parent = (ViewGroup) child.getParent(); @@ -285,7 +290,11 @@ public class ViewOverlay { } } - public void invalidateChildFast(View child, final Rect dirty) { + /** + * @hide + */ + @Override + public void damageChild(View child, final Rect dirty) { if (mHostView != null) { // Note: This is not a "fast" invalidation. Would be nice to instead invalidate // using DisplayList properties and a dirty rect instead of causing a real @@ -304,9 +313,9 @@ public class ViewOverlay { * @hide */ @Override - protected ViewParent invalidateChildInParentFast(int left, int top, Rect dirty) { + protected ViewParent damageChildInParent(int left, int top, Rect dirty) { if (mHostView instanceof ViewGroup) { - return ((ViewGroup) mHostView).invalidateChildInParentFast(left, top, dirty); + return ((ViewGroup) mHostView).damageChildInParent(left, top, dirty); } return null; } diff --git a/core/java/android/view/ViewPropertyAnimator.java b/core/java/android/view/ViewPropertyAnimator.java index 67a94be..6b21451 100644 --- a/core/java/android/view/ViewPropertyAnimator.java +++ b/core/java/android/view/ViewPropertyAnimator.java @@ -136,17 +136,18 @@ public class ViewPropertyAnimator { private static final int NONE = 0x0000; private static final int TRANSLATION_X = 0x0001; private static final int TRANSLATION_Y = 0x0002; - private static final int SCALE_X = 0x0004; - private static final int SCALE_Y = 0x0008; - private static final int ROTATION = 0x0010; - private static final int ROTATION_X = 0x0020; - private static final int ROTATION_Y = 0x0040; - private static final int X = 0x0080; - private static final int Y = 0x0100; - private static final int ALPHA = 0x0200; - - private static final int TRANSFORM_MASK = TRANSLATION_X | TRANSLATION_Y | SCALE_X | SCALE_Y | - ROTATION | ROTATION_X | ROTATION_Y | X | Y; + private static final int TRANSLATION_Z = 0x0004; + private static final int SCALE_X = 0x0008; + private static final int SCALE_Y = 0x0010; + private static final int ROTATION = 0x0020; + private static final int ROTATION_X = 0x0040; + private static final int ROTATION_Y = 0x0080; + private static final int X = 0x0100; + private static final int Y = 0x0200; + private static final int ALPHA = 0x0400; + + private static final int TRANSFORM_MASK = TRANSLATION_X | TRANSLATION_Y | TRANSLATION_Z | + SCALE_X | SCALE_Y | ROTATION | ROTATION_X | ROTATION_Y | X | Y; /** * The mechanism by which the user can request several properties that are then animated @@ -599,6 +600,31 @@ public class ViewPropertyAnimator { } /** + * This method will cause the View's <code>translationZ</code> property to be animated to the + * specified value. Animations already running on the property will be canceled. + * + * @param value The value to be animated to. + * @see View#setTranslationZ(float) + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimator translationZ(float value) { + animateProperty(TRANSLATION_Z, value); + return this; + } + + /** + * This method will cause the View's <code>translationZ</code> property to be animated by the + * specified value. Animations already running on the property will be canceled. + * + * @param value The amount to be animated by, as an offset from the current value. + * @see View#setTranslationZ(float) + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimator translationZBy(float value) { + animatePropertyBy(TRANSLATION_Z, value); + return this; + } + /** * This method will cause the View's <code>scaleX</code> property to be animated to the * specified value. Animations already running on the property will be canceled. * @@ -899,47 +925,41 @@ public class ViewPropertyAnimator { */ private void setValue(int propertyConstant, float value) { final View.TransformationInfo info = mView.mTransformationInfo; - final DisplayList displayList = mView.mDisplayList; + final RenderNode renderNode = mView.mRenderNode; switch (propertyConstant) { case TRANSLATION_X: - info.mTranslationX = value; - if (displayList != null) displayList.setTranslationX(value); + renderNode.setTranslationX(value); break; case TRANSLATION_Y: - info.mTranslationY = value; - if (displayList != null) displayList.setTranslationY(value); + renderNode.setTranslationY(value); + break; + case TRANSLATION_Z: + renderNode.setTranslationZ(value); break; case ROTATION: - info.mRotation = value; - if (displayList != null) displayList.setRotation(value); + renderNode.setRotation(value); break; case ROTATION_X: - info.mRotationX = value; - if (displayList != null) displayList.setRotationX(value); + renderNode.setRotationX(value); break; case ROTATION_Y: - info.mRotationY = value; - if (displayList != null) displayList.setRotationY(value); + renderNode.setRotationY(value); break; case SCALE_X: - info.mScaleX = value; - if (displayList != null) displayList.setScaleX(value); + renderNode.setScaleX(value); break; case SCALE_Y: - info.mScaleY = value; - if (displayList != null) displayList.setScaleY(value); + renderNode.setScaleY(value); break; case X: - info.mTranslationX = value - mView.mLeft; - if (displayList != null) displayList.setTranslationX(value - mView.mLeft); + renderNode.setTranslationX(value - mView.mLeft); break; case Y: - info.mTranslationY = value - mView.mTop; - if (displayList != null) displayList.setTranslationY(value - mView.mTop); + renderNode.setTranslationY(value - mView.mTop); break; case ALPHA: info.mAlpha = value; - if (displayList != null) displayList.setAlpha(value); + renderNode.setAlpha(value); break; } } @@ -951,28 +971,30 @@ public class ViewPropertyAnimator { * @return float The value of the named property */ private float getValue(int propertyConstant) { - final View.TransformationInfo info = mView.mTransformationInfo; + final RenderNode node = mView.mRenderNode; switch (propertyConstant) { case TRANSLATION_X: - return info.mTranslationX; + return node.getTranslationX(); case TRANSLATION_Y: - return info.mTranslationY; + return node.getTranslationY(); + case TRANSLATION_Z: + return node.getTranslationZ(); case ROTATION: - return info.mRotation; + return node.getRotation(); case ROTATION_X: - return info.mRotationX; + return node.getRotationX(); case ROTATION_Y: - return info.mRotationY; + return node.getRotationY(); case SCALE_X: - return info.mScaleX; + return node.getScaleX(); case SCALE_Y: - return info.mScaleY; + return node.getScaleY(); case X: - return mView.mLeft + info.mTranslationX; + return mView.mLeft + node.getTranslationX(); case Y: - return mView.mTop + info.mTranslationY; + return mView.mTop + node.getTranslationY(); case ALPHA: - return info.mAlpha; + return mView.mTransformationInfo.mAlpha; } return 0; } @@ -1061,7 +1083,7 @@ public class ViewPropertyAnimator { // Shouldn't happen, but just to play it safe return; } - boolean useDisplayListProperties = mView.mDisplayList != null; + boolean useRenderNodeProperties = mView.mRenderNode != null; // alpha requires slightly different treatment than the other (transform) properties. // The logic in setAlpha() is not simply setting mAlpha, plus the invalidation @@ -1069,7 +1091,7 @@ public class ViewPropertyAnimator { // We track what kinds of properties are set, and how alpha is handled when it is // set, and perform the invalidation steps appropriately. boolean alphaHandled = false; - if (!useDisplayListProperties) { + if (!useRenderNodeProperties) { mView.invalidateParentCaches(); } float fraction = animation.getAnimatedFraction(); @@ -1091,8 +1113,7 @@ public class ViewPropertyAnimator { } } if ((propertyMask & TRANSFORM_MASK) != 0) { - mView.mTransformationInfo.mMatrixDirty = true; - if (!useDisplayListProperties) { + if (!useRenderNodeProperties) { mView.mPrivateFlags |= View.PFLAG_DRAWN; // force another invalidation } } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 1c7f31c..ef22def 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -28,6 +28,7 @@ import android.content.res.CompatibilityInfo; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Canvas; +import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.Point; @@ -57,6 +58,7 @@ import android.util.DisplayMetrics; import android.util.Log; import android.util.Slog; import android.util.TypedValue; +import android.view.Surface.OutOfResourcesException; import android.view.View.AttachInfo; import android.view.View.MeasureSpec; import android.view.accessibility.AccessibilityEvent; @@ -70,7 +72,6 @@ import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.Interpolator; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; -import android.view.Surface.OutOfResourcesException; import android.widget.Scroller; import com.android.internal.R; @@ -110,7 +111,6 @@ public final class ViewRootImpl implements ViewParent, private static final boolean DEBUG_IMF = false || LOCAL_LOGV; private static final boolean DEBUG_CONFIGURATION = false || LOCAL_LOGV; private static final boolean DEBUG_FPS = false; - private static final boolean DEBUG_INPUT_PROCESSING = false || LOCAL_LOGV; /** * Set this system property to true to force the view hierarchy to render @@ -270,6 +270,10 @@ public final class ViewRootImpl implements ViewParent, HardwareLayer mResizeBuffer; long mResizeBufferStartTime; int mResizeBufferDuration; + // Used to block the creation of the ResizeBuffer due to invalidations in + // the previous DisplayList tree that must prevent re-execution. + // Currently this means a functor was detached. + boolean mBlockResizeBuffer; static final Interpolator mResizeInterpolator = new AccelerateDecelerateInterpolator(); private ArrayList<LayoutTransition> mPendingTransitions; @@ -293,8 +297,6 @@ public final class ViewRootImpl implements ViewParent, private long mFpsPrevTime = -1; private int mFpsNumFrames; - private final ArrayList<DisplayList> mDisplayLists = new ArrayList<DisplayList>(); - /** * see {@link #playSoundEffect(int)} */ @@ -621,7 +623,6 @@ public final class ViewRootImpl implements ViewParent, } void destroyHardwareResources() { - invalidateDisplayLists(); if (mAttachInfo.mHardwareRenderer != null) { mAttachInfo.mHardwareRenderer.destroyHardwareResources(mView); mAttachInfo.mHardwareRenderer.destroy(false); @@ -635,23 +636,25 @@ public final class ViewRootImpl implements ViewParent, HardwareRenderer.trimMemory(ComponentCallbacks2.TRIM_MEMORY_MODERATE); } } else { - invalidateDisplayLists(); - if (mAttachInfo.mHardwareRenderer != null && - mAttachInfo.mHardwareRenderer.isEnabled()) { - mAttachInfo.mHardwareRenderer.destroyLayers(mView); - } + destroyHardwareLayer(mView); } } - void pushHardwareLayerUpdate(HardwareLayer layer) { - if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) { - mAttachInfo.mHardwareRenderer.pushLayerUpdate(layer); + private static void destroyHardwareLayer(View view) { + view.destroyLayer(true); + + if (view instanceof ViewGroup) { + ViewGroup group = (ViewGroup) view; + + int count = group.getChildCount(); + for (int i = 0; i < count; i++) { + destroyHardwareLayer(group.getChildAt(i)); + } } } void flushHardwareLayerUpdates() { - if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled() && - mAttachInfo.mHardwareRenderer.validate()) { + if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) { mAttachInfo.mHardwareRenderer.flushLayerUpdates(); } } @@ -661,15 +664,15 @@ public final class ViewRootImpl implements ViewParent, mHandler.sendMessageAtFrontOfQueue(mHandler.obtainMessage(MSG_FLUSH_LAYER_UPDATES)); } - public boolean attachFunctor(long functor) { + public void attachFunctor(long functor) { //noinspection SimplifiableIfStatement if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) { - return mAttachInfo.mHardwareRenderer.attachFunctor(mAttachInfo, functor); + mAttachInfo.mHardwareRenderer.attachFunctor(mAttachInfo, functor); } - return false; } public void detachFunctor(long functor) { + mBlockResizeBuffer = true; if (mAttachInfo.mHardwareRenderer != null) { mAttachInfo.mHardwareRenderer.detachFunctor(functor); } @@ -720,7 +723,7 @@ public final class ViewRootImpl implements ViewParent, } final boolean translucent = attrs.format != PixelFormat.OPAQUE; - mAttachInfo.mHardwareRenderer = HardwareRenderer.createGlRenderer(2, translucent); + mAttachInfo.mHardwareRenderer = HardwareRenderer.create(translucent); if (mAttachInfo.mHardwareRenderer != null) { mAttachInfo.mHardwareRenderer.setName(attrs.getTitle().toString()); mAttachInfo.mHardwareAccelerated = @@ -960,14 +963,9 @@ public final class ViewRootImpl implements ViewParent, } void disposeResizeBuffer() { - if (mResizeBuffer != null && mAttachInfo.mHardwareRenderer != null) { - mAttachInfo.mHardwareRenderer.safelyRun(new Runnable() { - @Override - public void run() { - mResizeBuffer.destroy(); - mResizeBuffer = null; - } - }); + if (mResizeBuffer != null) { + mResizeBuffer.destroy(); + mResizeBuffer = null; } } @@ -1151,6 +1149,28 @@ public final class ViewRootImpl implements ViewParent, return windowSizeMayChange; } + /** + * Modifies the input matrix such that it maps view-local coordinates to + * on-screen coordinates. + * + * @param m input matrix to modify + */ + void transformMatrixToGlobal(Matrix m) { + final View.AttachInfo attachInfo = mAttachInfo; + m.postTranslate(attachInfo.mWindowLeft, attachInfo.mWindowTop); + } + + /** + * Modifies the input matrix such that it maps on-screen coordinates to + * view-local coordinates. + * + * @param m input matrix to modify + */ + void transformMatrixToLocal(Matrix m) { + final View.AttachInfo attachInfo = mAttachInfo; + m.preTranslate(-attachInfo.mWindowLeft, -attachInfo.mWindowTop); + } + private void performTraversals() { // cache mView since it is used so much below... final View host = mView; @@ -1221,11 +1241,6 @@ public final class ViewRootImpl implements ViewParent, desiredWindowHeight = packageMetrics.heightPixels; } - // For the very first time, tell the view hierarchy that it - // is attached to the window. Note that at this point the surface - // object is not initialized to its backing store, but soon it - // will be (assuming the window is visible). - attachInfo.mSurface = mSurface; // We used to use the following condition to choose 32 bits drawing caches: // PixelFormat.hasAlpha(lp.format) || lp.format == PixelFormat.RGBX_8888 // However, windows are now always 32 bits by default, so choose 32 bits @@ -1436,6 +1451,12 @@ public final class ViewRootImpl implements ViewParent, host.getMeasuredHeight() + ", params=" + params); } + if (mAttachInfo.mHardwareRenderer != null) { + // relayoutWindow may decide to destroy mSurface. As that decision + // happens in WindowManager service, we need to be defensive here + // and stop using the surface in case it gets destroyed. + mAttachInfo.mHardwareRenderer.pauseSurface(mSurface); + } final int surfaceGenerationId = mSurface.getGenerationId(); relayoutResult = relayoutWindow(params, viewVisibility, insetsPending); if (!mDrawDuringWindowsAnimating && @@ -1470,67 +1491,56 @@ public final class ViewRootImpl implements ViewParent, !mAttachInfo.mTurnOffWindowResizeAnim && mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled() && - mAttachInfo.mHardwareRenderer.validate() && - lp != null && !PixelFormat.formatHasAlpha(lp.format)) { + lp != null && !PixelFormat.formatHasAlpha(lp.format) + && !mBlockResizeBuffer) { disposeResizeBuffer(); - boolean completed = false; - HardwareCanvas hwRendererCanvas = mAttachInfo.mHardwareRenderer.getCanvas(); - HardwareCanvas layerCanvas = null; - try { - if (mResizeBuffer == null) { - mResizeBuffer = mAttachInfo.mHardwareRenderer.createHardwareLayer( - mWidth, mHeight, false); - } else if (mResizeBuffer.getWidth() != mWidth || - mResizeBuffer.getHeight() != mHeight) { - mResizeBuffer.resize(mWidth, mHeight); - } - // TODO: should handle create/resize failure - layerCanvas = mResizeBuffer.start(hwRendererCanvas); - final int restoreCount = layerCanvas.save(); - - int yoff; - final boolean scrolling = mScroller != null - && mScroller.computeScrollOffset(); - if (scrolling) { - yoff = mScroller.getCurrY(); - mScroller.abortAnimation(); - } else { - yoff = mScrollY; - } - - layerCanvas.translate(0, -yoff); - if (mTranslator != null) { - mTranslator.translateCanvas(layerCanvas); - } + if (mResizeBuffer == null) { + mResizeBuffer = mAttachInfo.mHardwareRenderer.createDisplayListLayer( + mWidth, mHeight); + } + mResizeBuffer.prepare(mWidth, mHeight, false); + RenderNode layerRenderNode = mResizeBuffer.startRecording(); + HardwareCanvas layerCanvas = layerRenderNode.start(mWidth, mHeight); + final int restoreCount = layerCanvas.save(); + + int yoff; + final boolean scrolling = mScroller != null + && mScroller.computeScrollOffset(); + if (scrolling) { + yoff = mScroller.getCurrY(); + mScroller.abortAnimation(); + } else { + yoff = mScrollY; + } - DisplayList displayList = mView.mDisplayList; - if (displayList != null && displayList.isValid()) { - layerCanvas.drawDisplayList(displayList, null, - DisplayList.FLAG_CLIP_CHILDREN); - } else { - mView.draw(layerCanvas); - } + layerCanvas.translate(0, -yoff); + if (mTranslator != null) { + mTranslator.translateCanvas(layerCanvas); + } - drawAccessibilityFocusedDrawableIfNeeded(layerCanvas); - - mResizeBufferStartTime = SystemClock.uptimeMillis(); - mResizeBufferDuration = mView.getResources().getInteger( - com.android.internal.R.integer.config_mediumAnimTime); - completed = true; - - layerCanvas.restoreToCount(restoreCount); - } catch (OutOfMemoryError e) { - Log.w(TAG, "Not enough memory for content change anim buffer", e); - } finally { - if (mResizeBuffer != null) { - mResizeBuffer.end(hwRendererCanvas); - if (!completed) { - disposeResizeBuffer(); - } - } + RenderNode renderNode = mView.mRenderNode; + if (renderNode != null && renderNode.isValid()) { + layerCanvas.drawDisplayList(renderNode, null, + RenderNode.FLAG_CLIP_CHILDREN); + } else { + mView.draw(layerCanvas); } + + drawAccessibilityFocusedDrawableIfNeeded(layerCanvas); + + mResizeBufferStartTime = SystemClock.uptimeMillis(); + mResizeBufferDuration = mView.getResources().getInteger( + com.android.internal.R.integer.config_mediumAnimTime); + + layerCanvas.restoreToCount(restoreCount); + layerRenderNode.end(layerCanvas); + layerRenderNode.setCaching(true); + layerRenderNode.setLeftTopRightBottom(0, 0, mWidth, mHeight); + mTempRect.set(0, 0, mWidth, mHeight); + mResizeBuffer.endRecording(mTempRect); + mAttachInfo.mHardwareRenderer.flushLayerUpdates(); } mAttachInfo.mContentInsets.set(mPendingContentInsets); if (DEBUG_LAYOUT) Log.v(TAG, "Content insets changing to: " @@ -1574,7 +1584,7 @@ public final class ViewRootImpl implements ViewParent, if (mAttachInfo.mHardwareRenderer != null) { try { hwInitialized = mAttachInfo.mHardwareRenderer.initialize( - mHolder.getSurface()); + mSurface); } catch (OutOfResourcesException e) { handleOutOfResourcesException(e); return; @@ -1601,7 +1611,7 @@ public final class ViewRootImpl implements ViewParent, mSurfaceHolder == null && mAttachInfo.mHardwareRenderer != null) { mFullRedrawNeeded = true; try { - mAttachInfo.mHardwareRenderer.updateSurface(mHolder.getSurface()); + mAttachInfo.mHardwareRenderer.updateSurface(mSurface); } catch (OutOfResourcesException e) { handleOutOfResourcesException(e); return; @@ -1684,7 +1694,7 @@ public final class ViewRootImpl implements ViewParent, mHeight != mAttachInfo.mHardwareRenderer.getHeight()) { mAttachInfo.mHardwareRenderer.setup(mWidth, mHeight); if (!hwInitialized) { - mAttachInfo.mHardwareRenderer.invalidate(mHolder.getSurface()); + mAttachInfo.mHardwareRenderer.invalidate(mSurface); mFullRedrawNeeded = true; } } @@ -2201,11 +2211,9 @@ public final class ViewRootImpl implements ViewParent, * @hide */ void outputDisplayList(View view) { - if (mAttachInfo != null && mAttachInfo.mHardwareCanvas != null) { - DisplayList displayList = view.getDisplayList(); - if (displayList != null) { - mAttachInfo.mHardwareCanvas.outputDisplayList(displayList); - } + RenderNode renderNode = view.getDisplayList(); + if (renderNode != null) { + renderNode.output(); } } @@ -2284,6 +2292,9 @@ public final class ViewRootImpl implements ViewParent, if (mReportNextDraw) { mReportNextDraw = false; + if (mAttachInfo.mHardwareRenderer != null) { + mAttachInfo.mHardwareRenderer.fence(); + } if (LOCAL_LOGV) { Log.v(TAG, "FINISHED DRAWING: " + mWindowAttributes.getTitle()); @@ -2390,8 +2401,6 @@ public final class ViewRootImpl implements ViewParent, appScale + ", width=" + mWidth + ", height=" + mHeight); } - invalidateDisplayLists(); - attachInfo.mTreeObserver.dispatchOnDraw(); if (!dirty.isEmpty() || mIsAnimating) { @@ -2404,6 +2413,7 @@ public final class ViewRootImpl implements ViewParent, mCurrentDirty.set(dirty); dirty.setEmpty(); + mBlockResizeBuffer = false; attachInfo.mHardwareRenderer.draw(mView, attachInfo, this, animating ? null : mCurrentDirty); } else { @@ -2421,7 +2431,7 @@ public final class ViewRootImpl implements ViewParent, try { attachInfo.mHardwareRenderer.initializeIfNeeded(mWidth, mHeight, - mHolder.getSurface()); + mSurface); } catch (OutOfResourcesException e) { handleOutOfResourcesException(e); return; @@ -2556,28 +2566,35 @@ public final class ViewRootImpl implements ViewParent, * @param canvas The canvas on which to draw. */ private void drawAccessibilityFocusedDrawableIfNeeded(Canvas canvas) { - AccessibilityManager manager = AccessibilityManager.getInstance(mView.mContext); + if (!mAttachInfo.mHasWindowFocus) { + return; + } + + final AccessibilityManager manager = AccessibilityManager.getInstance(mView.mContext); if (!manager.isEnabled() || !manager.isTouchExplorationEnabled()) { return; } - if (mAccessibilityFocusedHost == null || mAccessibilityFocusedHost.mAttachInfo == null) { + + final View host = mAccessibilityFocusedHost; + if (host == null || host.mAttachInfo == null) { return; } - Drawable drawable = getAccessibilityFocusedDrawable(); + + final Drawable drawable = getAccessibilityFocusedDrawable(); if (drawable == null) { return; } - AccessibilityNodeProvider provider = - mAccessibilityFocusedHost.getAccessibilityNodeProvider(); - Rect bounds = mView.mAttachInfo.mTmpInvalRect; + + final AccessibilityNodeProvider provider = host.getAccessibilityNodeProvider(); + final Rect bounds = mView.mAttachInfo.mTmpInvalRect; if (provider == null) { - mAccessibilityFocusedHost.getBoundsOnScreen(bounds); - } else { - if (mAccessibilityFocusedVirtualView == null) { - return; - } + host.getBoundsOnScreen(bounds); + } else if (mAccessibilityFocusedVirtualView != null) { mAccessibilityFocusedVirtualView.getBoundsInScreen(bounds); + } else { + return; } + bounds.offset(-mAttachInfo.mWindowLeft, -mAttachInfo.mWindowTop); bounds.intersect(0, 0, mAttachInfo.mViewRootImpl.mWidth, mAttachInfo.mViewRootImpl.mHeight); drawable.setBounds(bounds); @@ -2593,7 +2610,7 @@ public final class ViewRootImpl implements ViewParent, R.attr.accessibilityFocusedDrawable, value, true); if (resolved) { mAttachInfo.mAccessibilityFocusDrawable = - mView.mContext.getResources().getDrawable(value.resourceId); + mView.mContext.getDrawable(value.resourceId); } } return mAttachInfo.mAccessibilityFocusDrawable; @@ -2601,20 +2618,6 @@ public final class ViewRootImpl implements ViewParent, return null; } - void invalidateDisplayLists() { - final ArrayList<DisplayList> displayLists = mDisplayLists; - final int count = displayLists.size(); - - for (int i = 0; i < count; i++) { - final DisplayList displayList = displayLists.get(i); - if (displayList.isDirty()) { - displayList.reset(); - } - } - - displayLists.clear(); - } - /** * @hide */ @@ -2856,10 +2859,6 @@ public final class ViewRootImpl implements ViewParent, void dispatchDetachedFromWindow() { if (mView != null && mView.mAttachInfo != null) { - if (mAttachInfo.mHardwareRenderer != null && - mAttachInfo.mHardwareRenderer.isEnabled()) { - mAttachInfo.mHardwareRenderer.validate(); - } mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(false); mView.dispatchDetachedFromWindow(); } @@ -2876,7 +2875,6 @@ public final class ViewRootImpl implements ViewParent, mView.assignParent(null); mView = null; mAttachInfo.mRootView = null; - mAttachInfo.mSurface = null; mSurface.release(); @@ -3132,7 +3130,7 @@ public final class ViewRootImpl implements ViewParent, mFullRedrawNeeded = true; try { mAttachInfo.mHardwareRenderer.initializeIfNeeded( - mWidth, mHeight, mHolder.getSurface()); + mWidth, mHeight, mSurface); } catch (OutOfResourcesException e) { Log.e(TAG, "OutOfResourcesException locking surface", e); try { @@ -3181,8 +3179,6 @@ public final class ViewRootImpl implements ViewParent, mHasHadWindowFocus = true; } - setAccessibilityFocus(null, null); - if (mView != null && mAccessibilityManager.isEnabled()) { if (hasWindowFocus) { mView.sendAccessibilityEvent( @@ -3325,7 +3321,7 @@ public final class ViewRootImpl implements ViewParent, } else { // There's nothing to focus. Clear and propagate through the // hierarchy, but don't attempt to place new focus. - focused.clearFocusInternal(true, false); + focused.clearFocusInternal(null, true, false); return true; } } @@ -4529,8 +4525,7 @@ public final class ViewRootImpl implements ViewParent, // The active pointer id, or -1 if none. private int mActivePointerId = -1; - // Time and location where tracking started. - private long mStartTime; + // Location where tracking started. private float mStartX; private float mStartY; @@ -4558,9 +4553,6 @@ public final class ViewRootImpl implements ViewParent, private boolean mFlinging; private float mFlingVelocity; - // The last time a confirm key was pressed on the touch nav device - private long mLastConfirmKeyTime = Long.MAX_VALUE; - public SyntheticTouchNavigationHandler() { super(true); } @@ -4627,7 +4619,6 @@ public final class ViewRootImpl implements ViewParent, mActivePointerId = event.getPointerId(0); mVelocityTracker = VelocityTracker.obtain(); mVelocityTracker.addMovement(event); - mStartTime = time; mStartX = event.getX(); mStartY = event.getY(); mLastX = mStartX; @@ -5257,10 +5248,10 @@ public final class ViewRootImpl implements ViewParent, } private static void getGfxInfo(View view, int[] info) { - DisplayList displayList = view.mDisplayList; + RenderNode renderNode = view.mRenderNode; info[0]++; - if (displayList != null) { - info[1] += displayList.getSize(); + if (renderNode != null) { + info[1] += 0; /* TODO: Memory used by RenderNodes (properties + DisplayLists) */ } if (view instanceof ViewGroup) { @@ -5308,7 +5299,6 @@ public final class ViewRootImpl implements ViewParent, } if (mAdded && !mFirst) { - invalidateDisplayLists(); destroyHardwareRenderer(); if (mView != null) { @@ -5354,7 +5344,7 @@ public final class ViewRootImpl implements ViewParent, // Hardware rendering if (mAttachInfo.mHardwareRenderer != null) { - if (mAttachInfo.mHardwareRenderer.loadSystemProperties(mHolder.getSurface())) { + if (mAttachInfo.mHardwareRenderer.loadSystemProperties()) { invalidate(); } } @@ -5557,24 +5547,23 @@ public final class ViewRootImpl implements ViewParent, } private void deliverInputEvent(QueuedInputEvent q) { - Trace.traceBegin(Trace.TRACE_TAG_VIEW, "deliverInputEvent"); - try { - if (mInputEventConsistencyVerifier != null) { - mInputEventConsistencyVerifier.onInputEvent(q.mEvent, 0); - } + Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, "deliverInputEvent", + q.mEvent.getSequenceNumber()); + if (mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onInputEvent(q.mEvent, 0); + } - InputStage stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage; - if (stage != null) { - stage.deliver(q); - } else { - finishInputEvent(q); - } - } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIEW); + InputStage stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage; + if (stage != null) { + stage.deliver(q); + } else { + finishInputEvent(q); } } private void finishInputEvent(QueuedInputEvent q) { + Trace.asyncTraceEnd(Trace.TRACE_TAG_VIEW, "deliverInputEvent", + q.mEvent.getSequenceNumber()); if (q.mReceiver != null) { boolean handled = (q.mFlags & QueuedInputEvent.FLAG_FINISHED_HANDLED) != 0; q.mReceiver.finishInputEvent(q.mEvent, handled); @@ -5775,10 +5764,6 @@ public final class ViewRootImpl implements ViewParent, mInvalidateOnAnimationRunnable.addViewRect(info); } - public void enqueueDisplayList(DisplayList displayList) { - mDisplayLists.add(displayList); - } - public void cancelInvalidate(View view) { mHandler.removeMessages(MSG_INVALIDATE, view); // fixme: might leak the AttachInfo.InvalidateInfo objects instead of returning @@ -5801,6 +5786,9 @@ public final class ViewRootImpl implements ViewParent, public void dispatchUnhandledKey(KeyEvent event) { if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) { + // Some fallback keys are decided by the ViewRoot as they might have special + // properties (e.g. are locale aware). These take precedence over fallbacks defined by + // the kcm. final KeyCharacterMap kcm = event.getKeyCharacterMap(); final int keyCode = event.getKeyCode(); final int metaState = event.getMetaState(); @@ -5817,7 +5805,6 @@ public final class ViewRootImpl implements ViewParent, event.getDeviceId(), event.getScanCode(), flags, event.getSource(), null); fallbackAction.recycle(); - dispatchInputEvent(fallbackEvent); } } @@ -6287,68 +6274,6 @@ public final class ViewRootImpl implements ViewParent, } } - private final SurfaceHolder mHolder = new SurfaceHolder() { - // we only need a SurfaceHolder for opengl. it would be nice - // to implement everything else though, especially the callback - // support (opengl doesn't make use of it right now, but eventually - // will). - @Override - public Surface getSurface() { - return mSurface; - } - - @Override - public boolean isCreating() { - return false; - } - - @Override - public void addCallback(Callback callback) { - } - - @Override - public void removeCallback(Callback callback) { - } - - @Override - public void setFixedSize(int width, int height) { - } - - @Override - public void setSizeFromLayout() { - } - - @Override - public void setFormat(int format) { - } - - @Override - public void setType(int type) { - } - - @Override - public void setKeepScreenOn(boolean screenOn) { - } - - @Override - public Canvas lockCanvas() { - return null; - } - - @Override - public Canvas lockCanvas(Rect dirty) { - return null; - } - - @Override - public void unlockCanvasAndPost(Canvas canvas) { - } - @Override - public Rect getSurfaceFrame() { - return null; - } - }; - static RunQueue getRunQueue() { RunQueue rq = sRunQueues.get(); if (rq != null) { @@ -6458,7 +6383,7 @@ public final class ViewRootImpl implements ViewParent, public void ensureConnection() { if (mAttachInfo != null) { final boolean registered = - mAttachInfo.mAccessibilityWindowId != AccessibilityNodeInfo.UNDEFINED; + mAttachInfo.mAccessibilityWindowId != AccessibilityNodeInfo.UNDEFINED_ITEM_ID; if (!registered) { mAttachInfo.mAccessibilityWindowId = mAccessibilityManager.addAccessibilityInteractionConnection(mWindow, @@ -6469,9 +6394,9 @@ public final class ViewRootImpl implements ViewParent, public void ensureNoConnection() { final boolean registered = - mAttachInfo.mAccessibilityWindowId != AccessibilityNodeInfo.UNDEFINED; + mAttachInfo.mAccessibilityWindowId != AccessibilityNodeInfo.UNDEFINED_ITEM_ID; if (registered) { - mAttachInfo.mAccessibilityWindowId = AccessibilityNodeInfo.UNDEFINED; + mAttachInfo.mAccessibilityWindowId = AccessibilityNodeInfo.UNDEFINED_ITEM_ID; mAccessibilityManager.removeAccessibilityInteractionConnection(mWindow); } } diff --git a/core/java/android/view/ViewStub.java b/core/java/android/view/ViewStub.java index a5dc3ae..d68a860 100644 --- a/core/java/android/view/ViewStub.java +++ b/core/java/android/view/ViewStub.java @@ -97,16 +97,21 @@ public final class ViewStub extends View { } @SuppressWarnings({"UnusedDeclaration"}) - public ViewStub(Context context, AttributeSet attrs, int defStyle) { - TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.ViewStub, - defStyle, 0); + public ViewStub(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.ViewStub, defStyleAttr, defStyleRes); mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID); mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0); a.recycle(); - a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.View, defStyle, 0); + a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes); mID = a.getResourceId(R.styleable.View_id, NO_ID); a.recycle(); diff --git a/core/java/android/view/VolumePanel.java b/core/java/android/view/VolumePanel.java index 52f9c0b..4730e59 100644 --- a/core/java/android/view/VolumePanel.java +++ b/core/java/android/view/VolumePanel.java @@ -56,10 +56,8 @@ import java.util.HashMap; * * @hide */ -public class VolumePanel extends Handler implements OnSeekBarChangeListener, View.OnClickListener, - VolumeController -{ - private static final String TAG = "VolumePanel"; +public class VolumePanel extends Handler implements VolumeController { + private static final String TAG = VolumePanel.class.getSimpleName(); private static boolean LOGD = false; /** @@ -187,7 +185,7 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie this.iconMuteRes = iconMuteRes; this.show = show; } - }; + } // List of stream types and their order private static final StreamResources[] STREAMS = { @@ -238,6 +236,7 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie cleanUp(); } + @Override public void onDismiss(DialogInterface unused) { mContext.unregisterReceiver(this); cleanUp(); @@ -253,14 +252,14 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie } - public VolumePanel(final Context context, AudioService volumeService) { + public VolumePanel(Context context, AudioService volumeService) { mContext = context; mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); mAudioService = volumeService; // For now, only show master volume if master volume is supported - boolean useMasterVolume = context.getResources().getBoolean( - com.android.internal.R.bool.config_useMasterVolume); + final Resources res = context.getResources(); + final boolean useMasterVolume = res.getBoolean(R.bool.config_useMasterVolume); if (useMasterVolume) { for (int i = 0; i < STREAMS.length; i++) { StreamResources streamRes = STREAMS[i]; @@ -268,21 +267,8 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie } } - LayoutInflater inflater = (LayoutInflater) context - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - View view = mView = inflater.inflate(R.layout.volume_adjust, null); - mView.setOnTouchListener(new View.OnTouchListener() { - public boolean onTouch(View v, MotionEvent event) { - resetTimeout(); - return false; - } - }); - mPanel = (ViewGroup) mView.findViewById(R.id.visible_panel); - mSliderGroup = (ViewGroup) mView.findViewById(R.id.slider_group); - mMoreButton = (ImageView) mView.findViewById(R.id.expand_button); - mDivider = (ImageView) mView.findViewById(R.id.expand_button_divider); - - mDialog = new Dialog(context, R.style.Theme_Panel_Volume) { + mDialog = new Dialog(context) { + @Override public boolean onTouchEvent(MotionEvent event) { if (isShowing() && event.getAction() == MotionEvent.ACTION_OUTSIDE && sConfirmSafeVolumeDialog == null) { @@ -292,47 +278,65 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie return false; } }; - mDialog.setTitle("Volume control"); // No need to localize - mDialog.setContentView(mView); - mDialog.setOnDismissListener(new OnDismissListener() { - public void onDismiss(DialogInterface dialog) { - mActiveStreamType = -1; - mAudioManager.forceVolumeControlStream(mActiveStreamType); - } - }); + // Change some window properties - Window window = mDialog.getWindow(); - window.setGravity(Gravity.TOP); - LayoutParams lp = window.getAttributes(); + final Window window = mDialog.getWindow(); + final LayoutParams lp = window.getAttributes(); lp.token = null; // Offset from the top - lp.y = mContext.getResources().getDimensionPixelOffset( - com.android.internal.R.dimen.volume_panel_top); + lp.y = res.getDimensionPixelOffset(R.dimen.volume_panel_top); lp.type = LayoutParams.TYPE_VOLUME_OVERLAY; - lp.width = LayoutParams.WRAP_CONTENT; - lp.height = LayoutParams.WRAP_CONTENT; + lp.windowAnimations = R.style.Animation_VolumePanel; window.setAttributes(lp); - window.addFlags(LayoutParams.FLAG_NOT_FOCUSABLE | LayoutParams.FLAG_NOT_TOUCH_MODAL + window.setGravity(Gravity.TOP); + window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); + window.requestFeature(Window.FEATURE_NO_TITLE); + window.addFlags(LayoutParams.FLAG_NOT_FOCUSABLE + | LayoutParams.FLAG_NOT_TOUCH_MODAL | LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH); - mToneGenerators = new ToneGenerator[AudioSystem.getNumStreamTypes()]; - mVibrator = (Vibrator)context.getSystemService(Context.VIBRATOR_SERVICE); + mDialog.setCanceledOnTouchOutside(true); + mDialog.setContentView(R.layout.volume_adjust); + mDialog.setOnDismissListener(new OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + mActiveStreamType = -1; + mAudioManager.forceVolumeControlStream(mActiveStreamType); + } + }); + + mDialog.create(); + mView = window.findViewById(R.id.content); + mView.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + resetTimeout(); + return false; + } + }); + + mPanel = (ViewGroup) mView.findViewById(R.id.visible_panel); + mSliderGroup = (ViewGroup) mView.findViewById(R.id.slider_group); + mMoreButton = mView.findViewById(R.id.expand_button); + mDivider = mView.findViewById(R.id.expand_button_divider); + + mToneGenerators = new ToneGenerator[AudioSystem.getNumStreamTypes()]; + mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); mVoiceCapable = context.getResources().getBoolean(R.bool.config_voice_capable); + + // If we don't want to show multiple volumes, hide the settings button + // and divider. mShowCombinedVolumes = !mVoiceCapable && !useMasterVolume; - // If we don't want to show multiple volumes, hide the settings button and divider if (!mShowCombinedVolumes) { mMoreButton.setVisibility(View.GONE); mDivider.setVisibility(View.GONE); } else { - mMoreButton.setOnClickListener(this); + mMoreButton.setOnClickListener(mClickListener); } - boolean masterVolumeOnly = context.getResources().getBoolean( - com.android.internal.R.bool.config_useMasterVolume); - boolean masterVolumeKeySounds = mContext.getResources().getBoolean( - com.android.internal.R.bool.config_useVolumeKeySounds); - + final boolean masterVolumeOnly = res.getBoolean(R.bool.config_useMasterVolume); + final boolean masterVolumeKeySounds = res.getBoolean(R.bool.config_useVolumeKeySounds); mPlayMasterStreamTones = masterVolumeOnly && masterVolumeKeySounds; listenToRingerMode(); @@ -347,7 +351,7 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie final IntentFilter filter = new IntentFilter(); filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION); mContext.registerReceiver(new BroadcastReceiver() { - + @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); @@ -400,17 +404,21 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie } private void createSliders() { - LayoutInflater inflater = (LayoutInflater) mContext - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + final Resources res = mContext.getResources(); + final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + mStreamControls = new HashMap<Integer, StreamControl>(STREAMS.length); - Resources res = mContext.getResources(); + for (int i = 0; i < STREAMS.length; i++) { StreamResources streamRes = STREAMS[i]; - int streamType = streamRes.streamType; + + final int streamType = streamRes.streamType; if (mVoiceCapable && streamRes == StreamResources.NotificationStream) { streamRes = StreamResources.RingerStream; } - StreamControl sc = new StreamControl(); + + final StreamControl sc = new StreamControl(); sc.streamType = streamType; sc.group = (ViewGroup) inflater.inflate(R.layout.volume_adjust_item, null); sc.group.setTag(sc); @@ -421,10 +429,10 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie sc.iconMuteRes = streamRes.iconMuteRes; sc.icon.setImageResource(sc.iconRes); sc.seekbarView = (SeekBar) sc.group.findViewById(R.id.seekbar); - int plusOne = (streamType == AudioSystem.STREAM_BLUETOOTH_SCO || + final int plusOne = (streamType == AudioSystem.STREAM_BLUETOOTH_SCO || streamType == AudioSystem.STREAM_VOICE_CALL) ? 1 : 0; sc.seekbarView.setMax(getStreamMaxVolume(streamType) + plusOne); - sc.seekbarView.setOnSeekBarChangeListener(this); + sc.seekbarView.setOnSeekBarChangeListener(mSeekListener); sc.seekbarView.setTag(sc); mStreamControls.put(streamType, sc); } @@ -433,7 +441,7 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie private void reorderSliders(int activeStreamType) { mSliderGroup.removeAllViews(); - StreamControl active = mStreamControls.get(activeStreamType); + final StreamControl active = mStreamControls.get(activeStreamType); if (active == null) { Log.e("VolumePanel", "Missing stream type! - " + activeStreamType); mActiveStreamType = -1; @@ -486,10 +494,6 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie } } - private boolean isExpanded() { - return mMoreButton.getVisibility() != View.VISIBLE; - } - private void expand() { final int count = mSliderGroup.getChildCount(); for (int i = 0; i < count; i++) { @@ -527,6 +531,7 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie obtainMessage(MSG_VOLUME_CHANGED, streamType, flags).sendToTarget(); } + @Override public void postRemoteVolumeChanged(int streamType, int flags) { if (hasMessages(MSG_REMOTE_VOLUME_CHANGED)) return; synchronized (this) { @@ -538,6 +543,7 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie obtainMessage(MSG_REMOTE_VOLUME_CHANGED, streamType, flags).sendToTarget(); } + @Override public void postRemoteSliderVisibility(boolean visible) { obtainMessage(MSG_SLIDER_VISIBILITY_CHANGED, AudioService.STREAM_REMOTE_MUSIC, visible ? 1 : 0).sendToTarget(); @@ -554,6 +560,7 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie * as a request to update the volume), the application will likely set a new volume. If the UI * is still up, we need to refresh the display to show this new value. */ + @Override public void postHasNewRemotePlaybackInfo() { if (hasMessages(MSG_REMOTE_VOLUME_UPDATE_IF_SHOWN)) return; // don't create or prevent resources to be freed, if they disappear, this update came too @@ -733,7 +740,7 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie int stream = (streamType == AudioService.STREAM_REMOTE_MUSIC) ? -1 : streamType; // when the stream is for remote playback, use -1 to reset the stream type evaluation mAudioManager.forceVolumeControlStream(stream); - mDialog.setContentView(mView); + // Showing dialog - use collapsed state if (mShowCombinedVolumes) { collapse(); @@ -787,7 +794,7 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie return; } - mVibrator.vibrate(VIBRATE_DURATION); + mVibrator.vibrate(VIBRATE_DURATION, AudioManager.STREAM_SYSTEM); } protected void onRemoteVolumeChanged(int streamType, int flags) { @@ -865,6 +872,7 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie .setMessage(com.android.internal.R.string.safe_media_volume_warning) .setPositiveButton(com.android.internal.R.string.yes, new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int which) { mAudioService.disableSafeMediaVolume(); } @@ -1021,39 +1029,48 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie sendMessage(obtainMessage(MSG_TIMEOUT)); } - public void onProgressChanged(SeekBar seekBar, int progress, - boolean fromUser) { - final Object tag = seekBar.getTag(); - if (fromUser && tag instanceof StreamControl) { - StreamControl sc = (StreamControl) tag; - if (getStreamVolume(sc.streamType) != progress) { - setStreamVolume(sc.streamType, progress, 0); + private final OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + final Object tag = seekBar.getTag(); + if (fromUser && tag instanceof StreamControl) { + StreamControl sc = (StreamControl) tag; + if (getStreamVolume(sc.streamType) != progress) { + setStreamVolume(sc.streamType, progress, 0); + } } + resetTimeout(); } - resetTimeout(); - } - public void onStartTrackingTouch(SeekBar seekBar) { - } + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } - public void onStopTrackingTouch(SeekBar seekBar) { - final Object tag = seekBar.getTag(); - if (tag instanceof StreamControl) { - StreamControl sc = (StreamControl) tag; - // because remote volume updates are asynchronous, AudioService might have received - // a new remote volume value since the finger adjusted the slider. So when the - // progress of the slider isn't being tracked anymore, adjust the slider to the last - // "published" remote volume value, so the UI reflects the actual volume. - if (sc.streamType == AudioService.STREAM_REMOTE_MUSIC) { - seekBar.setProgress(getStreamVolume(AudioService.STREAM_REMOTE_MUSIC)); + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + final Object tag = seekBar.getTag(); + if (tag instanceof StreamControl) { + StreamControl sc = (StreamControl) tag; + // Because remote volume updates are asynchronous, AudioService + // might have received a new remote volume value since the + // finger adjusted the slider. So when the progress of the + // slider isn't being tracked anymore, adjust the slider to the + // last "published" remote volume value, so the UI reflects the + // actual volume. + if (sc.streamType == AudioService.STREAM_REMOTE_MUSIC) { + seekBar.setProgress(getStreamVolume(AudioService.STREAM_REMOTE_MUSIC)); + } } } - } + }; - public void onClick(View v) { - if (v == mMoreButton) { - expand(); + private final View.OnClickListener mClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + if (v == mMoreButton) { + expand(); + } + resetTimeout(); } - resetTimeout(); - } + }; } diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java index c450f3c..7bd1f56 100644 --- a/core/java/android/view/Window.java +++ b/core/java/android/view/Window.java @@ -16,6 +16,8 @@ package android.view; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; import android.content.res.Configuration; import android.content.res.TypedArray; @@ -25,8 +27,13 @@ import android.net.Uri; import android.os.Bundle; import android.os.IBinder; import android.os.SystemProperties; +import android.transition.Scene; +import android.transition.Transition; +import android.transition.TransitionManager; import android.view.accessibility.AccessibilityEvent; +import java.util.Map; + /** * Abstract base class for a top-level window look and behavior policy. An * instance of this class should be used as the top-level view added to the @@ -89,17 +96,25 @@ public abstract class Window { * If overlay is enabled, the action mode UI will be allowed to cover existing window content. */ public static final int FEATURE_ACTION_MODE_OVERLAY = 10; - /** * Flag for requesting a decoration-free window that is dismissed by swiping from the left. */ public static final int FEATURE_SWIPE_TO_DISMISS = 11; + /** + * Flag for requesting that window content changes should be represented + * with scenes and transitions. + * + * TODO Add docs + * + * @see #setContentView + */ + public static final int FEATURE_CONTENT_TRANSITIONS = 12; /** * Max value used as a feature ID * @hide */ - public static final int FEATURE_MAX = FEATURE_SWIPE_TO_DISMISS; + public static final int FEATURE_MAX = FEATURE_CONTENT_TRANSITIONS; /** Flag for setting the progress bar's visibility to VISIBLE */ public static final int PROGRESS_VISIBILITY_ON = -1; @@ -245,6 +260,7 @@ public abstract class Window { * * @see #onPreparePanel */ + @Nullable public View onCreatePanelView(int featureId); /** @@ -373,6 +389,7 @@ public abstract class Window { * @param callback Callback to control the lifecycle of this action mode * @return The ActionMode that was started, or null if the system should present it */ + @Nullable public ActionMode onWindowStartingActionMode(ActionMode.Callback callback); /** @@ -980,6 +997,7 @@ public abstract class Window { * * @return View The current View with focus or null. */ + @Nullable public abstract View getCurrentFocus(); /** @@ -988,10 +1006,12 @@ public abstract class Window { * * @return LayoutInflater The shared LayoutInflater. */ + @NonNull public abstract LayoutInflater getLayoutInflater(); public abstract void setTitle(CharSequence title); + @Deprecated public abstract void setTitleColor(int textColor); public abstract void openPanel(int featureId, KeyEvent event); @@ -1032,7 +1052,7 @@ public abstract class Window { */ public void setBackgroundDrawableResource(int resid) { - setBackgroundDrawable(mContext.getResources().getDrawable(resid)); + setBackgroundDrawable(mContext.getDrawable(resid)); } /** @@ -1328,4 +1348,123 @@ public abstract class Window { * @param event A key or touch event to inject to this window. */ public void injectInputEvent(InputEvent event) { } + + /** + * Retrieve the {@link TransitionManager} responsible for for default transitions + * in this window. Requires {@link #FEATURE_CONTENT_TRANSITIONS}. + * + * <p>This method will return non-null after content has been initialized (e.g. by using + * {@link #setContentView}) if {@link #FEATURE_CONTENT_TRANSITIONS} has been granted.</p> + * + * @return This window's content TransitionManager or null if none is set. + */ + public TransitionManager getTransitionManager() { + return null; + } + + /** + * Set the {@link TransitionManager} to use for default transitions in this window. + * Requires {@link #FEATURE_CONTENT_TRANSITIONS}. + * + * @param tm The TransitionManager to use for scene changes. + */ + public void setTransitionManager(TransitionManager tm) { + throw new UnsupportedOperationException(); + } + + /** + * Retrieve the {@link Scene} representing this window's current content. + * Requires {@link #FEATURE_CONTENT_TRANSITIONS}. + * + * <p>This method will return null if the current content is not represented by a Scene.</p> + * + * @return Current Scene being shown or null + */ + public Scene getContentScene() { + return null; + } + + /** + * Set options that can affect the transition behavior within this window. + * @param options Options to set or null for none + * @hide + */ + public void setTransitionOptions(Bundle options, SceneTransitionListener listener) { + } + + /** + * A callback for Window transitions to be told when the shared element is ready to be shown + * and start the transition to its target location. + * @hide + */ + public interface SceneTransitionListener { + void nullPendingTransition(); + void convertFromTranslucent(); + void convertToTranslucent(); + void sharedElementStart(Transition transition); + void sharedElementEnd(); + } + + /** + * Controls how the Activity's start Scene is faded in and when the enter scene + * is triggered to start. + * <p>When allow is true, the enter Scene will begin as soon as possible + * and the background will fade in when all shared elements are ready to begin + * transitioning. If allow is false, the Activity enter Scene and + * background fade will be triggered when the calling Activity's exit transition + * completes.</p> + * @param allow Set to true to have the Activity enter scene transition in + * as early as possible or set to false to wait for the calling + * Activity to exit first. The default value is true. + */ + public void setAllowOverlappingEnterTransition(boolean allow) { + } + + /** + * Controls how the Activity's Scene fades out and when the calling Activity's + * enter scene is triggered when finishing to return to a calling Activity. + * <p>When allow is true, the Scene will fade out quickly + * and inform the calling Activity to transition in when the fade completes. + * When allow is false, the calling Activity will transition in after + * the Activity's Scene has exited. + * </p> + * @param allow Set to true to have the Activity fade out as soon as possible + * and transition in the calling Activity. The default value is + * true. + */ + public void setAllowOverlappingExitTransition(boolean allow) { + } + + /** + * Start the exit transition. + * @hide + */ + public Bundle startExitTransitionToCallee(Bundle options) { + return null; + } + + /** + * Starts the transition back to the calling Activity. + * onTransitionEnd will be called on the current thread if there is no exit transition. + * @hide + */ + public void startExitTransitionToCaller(Runnable onTransitionEnd) { + onTransitionEnd.run(); + } + + /** @hide */ + public void restoreViewVisibilityAfterTransitionToCallee() { + } + + /** + * On entering Activity Scene transitions, shared element names may be mapped from a + * source Activity's specified name to a unique shared element name in the View hierarchy. + * Under most circumstances, mapping is not necessary - a single View will have the + * shared element name given by the calling Activity. However, if there are several similar + * Views (e.g. in a ListView), the correct shared element must be mapped. + * @param sharedElementNames A mapping from the calling Activity's assigned shared element + * name to a unique shared element name in the View hierarchy. + */ + public void mapTransitionTargets(Map<String, String> sharedElementNames) { + } } diff --git a/core/java/android/view/WindowAnimationFrameStats.aidl b/core/java/android/view/WindowAnimationFrameStats.aidl new file mode 100644 index 0000000..77f544b --- /dev/null +++ b/core/java/android/view/WindowAnimationFrameStats.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2014, 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.view; + +parcelable WindowAnimationFrameStats; diff --git a/core/java/android/view/WindowAnimationFrameStats.java b/core/java/android/view/WindowAnimationFrameStats.java new file mode 100644 index 0000000..c60b96c --- /dev/null +++ b/core/java/android/view/WindowAnimationFrameStats.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2014 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.view; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * This class contains window animation frame statistics. For example, a window + * animation is usually performed when the application is transitioning from one + * activity to another. The frame statistics are a snapshot for the time interval + * from {@link #getStartTimeNano()} to {@link #getEndTimeNano()}. + * <p> + * The key idea is that in order to provide a smooth user experience the system should + * run window animations at a specific time interval obtained by calling {@link + * #getRefreshPeriodNano()}. If the system does not render a frame every refresh + * period the user will see irregular window transitions. The time when the frame was + * actually presented on the display by calling {@link #getFramePresentedTimeNano(int)}. + */ +public final class WindowAnimationFrameStats extends FrameStats implements Parcelable { + /** + * @hide + */ + public WindowAnimationFrameStats() { + /* do nothing */ + } + + /** + * Initializes this isntance. + * + * @param refreshPeriodNano The display refresh period. + * @param framesPresentedTimeNano The presented frame times. + * + * @hide + */ + public void init(long refreshPeriodNano, long[] framesPresentedTimeNano) { + mRefreshPeriodNano = refreshPeriodNano; + mFramesPresentedTimeNano = framesPresentedTimeNano; + } + + private WindowAnimationFrameStats(Parcel parcel) { + mRefreshPeriodNano = parcel.readLong(); + mFramesPresentedTimeNano = parcel.createLongArray(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeLong(mRefreshPeriodNano); + parcel.writeLongArray(mFramesPresentedTimeNano); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("WindowAnimationFrameStats["); + builder.append("frameCount:" + getFrameCount()); + builder.append(", fromTimeNano:" + getStartTimeNano()); + builder.append(", toTimeNano:" + getEndTimeNano()); + builder.append(']'); + return builder.toString(); + } + + public static final Creator<WindowAnimationFrameStats> CREATOR = + new Creator<WindowAnimationFrameStats>() { + @Override + public WindowAnimationFrameStats createFromParcel(Parcel parcel) { + return new WindowAnimationFrameStats(parcel); + } + + @Override + public WindowAnimationFrameStats[] newArray(int size) { + return new WindowAnimationFrameStats[size]; + } + }; +} diff --git a/core/java/android/view/WindowContentFrameStats.aidl b/core/java/android/view/WindowContentFrameStats.aidl new file mode 100644 index 0000000..aa9c2d6 --- /dev/null +++ b/core/java/android/view/WindowContentFrameStats.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2014, 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.view; + +parcelable WindowContentFrameStats; diff --git a/core/java/android/view/WindowContentFrameStats.java b/core/java/android/view/WindowContentFrameStats.java new file mode 100644 index 0000000..c6da2fb --- /dev/null +++ b/core/java/android/view/WindowContentFrameStats.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2014 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.view; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * This class contains window content frame statistics. For example, a window content + * is rendred in frames when a view is scrolled. The frame statistics are a snapshot + * for the time interval from {@link #getStartTimeNano()} to {@link #getEndTimeNano()}. + * <p> + * The key idea is that in order to provide a smooth user experience an application + * has to draw a frame at a specific time interval obtained by calling {@link + * #getRefreshPeriodNano()}. If the application does not render a frame every refresh + * period the user will see irregular UI transitions. + * </p> + * <p> + * An application posts a frame for presentation by synchronously rendering its contents + * in a buffer which is then posted or posting a buffer to which the application is + * asychronously rendering the content via GL. After the frame is posted and rendered + * (potentially asynchronosly) it is presented to the user. The time a frame was posted + * can be obtained via {@link #getFramePostedTimeNano(int)}, the time a frame content + * was rendered and ready for dsiplay (GL case) via {@link #getFrameReadyTimeNano(int)}, + * and the time a frame was presented on the screen via {@link #getFramePresentedTimeNano(int)}. + * </p> + */ +public final class WindowContentFrameStats extends FrameStats implements Parcelable { + private long[] mFramesPostedTimeNano; + private long[] mFramesReadyTimeNano; + + /** + * @hide + */ + public WindowContentFrameStats() { + /* do nothing */ + } + + /** + * Initializes this isntance. + * + * @param refreshPeriodNano The display refresh period. + * @param framesPostedTimeNano The times in milliseconds for when the frame contents were posted. + * @param framesPresentedTimeNano The times in milliseconds for when the frame contents were presented. + * @param framesReadyTimeNano The times in milliseconds for when the frame contents were ready to be presented. + * + * @hide + */ + public void init(long refreshPeriodNano, long[] framesPostedTimeNano, + long[] framesPresentedTimeNano, long[] framesReadyTimeNano) { + mRefreshPeriodNano = refreshPeriodNano; + mFramesPostedTimeNano = framesPostedTimeNano; + mFramesPresentedTimeNano = framesPresentedTimeNano; + mFramesReadyTimeNano = framesReadyTimeNano; + } + + private WindowContentFrameStats(Parcel parcel) { + mRefreshPeriodNano = parcel.readLong(); + mFramesPostedTimeNano = parcel.createLongArray(); + mFramesPresentedTimeNano = parcel.createLongArray(); + mFramesReadyTimeNano = parcel.createLongArray(); + } + + /** + * Get the time a frame at a given index was posted by the producer (e.g. the application). + * It is either explicitly set or defaulted to the time when the render buffer was posted. + * <p> + * <strong>Note:</strong> A frame can be posted and still it contents being rendered + * asynchronously in GL. To get the time the frame content was completely rendered and + * ready to display call {@link #getFrameReadyTimeNano(int)}. + * </p> + * + * @param index The frame index. + * @return The posted time in nanoseconds. + */ + public long getFramePostedTimeNano(int index) { + if (mFramesPostedTimeNano == null) { + throw new IndexOutOfBoundsException(); + } + return mFramesPostedTimeNano[index]; + } + + /** + * Get the time a frame at a given index was ready for presentation. + * <p> + * <strong>Note:</strong> A frame can be posted and still it contents being rendered + * asynchronously in GL. In such a case this is the time when the frame contents were + * completely rendered. + * </p> + * + * @param index The frame index. + * @return The ready time in nanoseconds or {@link #UNDEFINED_TIME_NANO} + * if the frame is not ready yet. + */ + public long getFrameReadyTimeNano(int index) { + if (mFramesReadyTimeNano == null) { + throw new IndexOutOfBoundsException(); + } + return mFramesReadyTimeNano[index]; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeLong(mRefreshPeriodNano); + parcel.writeLongArray(mFramesPostedTimeNano); + parcel.writeLongArray(mFramesPresentedTimeNano); + parcel.writeLongArray(mFramesReadyTimeNano); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("WindowContentFrameStats["); + builder.append("frameCount:" + getFrameCount()); + builder.append(", fromTimeNano:" + getStartTimeNano()); + builder.append(", toTimeNano:" + getEndTimeNano()); + builder.append(']'); + return builder.toString(); + } + + public static final Parcelable.Creator<WindowContentFrameStats> CREATOR = + new Creator<WindowContentFrameStats>() { + @Override + public WindowContentFrameStats createFromParcel(Parcel parcel) { + return new WindowContentFrameStats(parcel); + } + + @Override + public WindowContentFrameStats[] newArray(int size) { + return new WindowContentFrameStats[size]; + } + }; +} diff --git a/core/java/android/view/WindowInfo.aidl b/core/java/android/view/WindowInfo.aidl new file mode 100644 index 0000000..75b8fd2 --- /dev/null +++ b/core/java/android/view/WindowInfo.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2014, 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.view; + +parcelable WindowInfo; diff --git a/core/java/android/view/WindowInfo.java b/core/java/android/view/WindowInfo.java new file mode 100644 index 0000000..7f89044 --- /dev/null +++ b/core/java/android/view/WindowInfo.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2014 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.view; + +import android.graphics.Rect; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Pools; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class represents information about a window from the + * window manager to another part of the system. + * + * @hide + */ +public class WindowInfo implements Parcelable { + private static final int MAX_POOL_SIZE = 10; + + private static final Pools.SynchronizedPool<WindowInfo> sPool = + new Pools.SynchronizedPool<WindowInfo>(MAX_POOL_SIZE); + + public int type; + public int layer; + public IBinder token; + public IBinder parentToken; + public boolean focused; + public final Rect boundsInScreen = new Rect(); + public List<IBinder> childTokens; + + private WindowInfo() { + /* do nothing - hide constructor */ + } + + public static WindowInfo obtain() { + WindowInfo window = sPool.acquire(); + if (window == null) { + window = new WindowInfo(); + } + return window; + } + + public static WindowInfo obtain(WindowInfo other) { + WindowInfo window = obtain(); + window.type = other.type; + window.layer = other.layer; + window.token = other.token; + window.parentToken = other.parentToken; + window.focused = other.focused; + window.boundsInScreen.set(other.boundsInScreen); + + if (other.childTokens != null && !other.childTokens.isEmpty()) { + if (window.childTokens == null) { + window.childTokens = new ArrayList<IBinder>(other.childTokens); + } else { + window.childTokens.addAll(other.childTokens); + } + } + + return window; + } + + public void recycle() { + clear(); + sPool.release(this); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeInt(type); + parcel.writeInt(layer); + parcel.writeStrongBinder(token); + parcel.writeStrongBinder(parentToken); + parcel.writeInt(focused ? 1 : 0); + boundsInScreen.writeToParcel(parcel, flags); + + if (childTokens != null && !childTokens.isEmpty()) { + parcel.writeInt(1); + parcel.writeBinderList(childTokens); + } else { + parcel.writeInt(0); + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("WindowInfo["); + builder.append("type=").append(type); + builder.append(", layer=").append(layer); + builder.append(", token=").append(token); + builder.append(", parent=").append(parentToken); + builder.append(", focused=").append(focused); + builder.append(", children=").append(childTokens); + builder.append(']'); + return builder.toString(); + } + + private void initFromParcel(Parcel parcel) { + type = parcel.readInt(); + layer = parcel.readInt(); + token = parcel.readStrongBinder(); + parentToken = parcel.readStrongBinder(); + focused = (parcel.readInt() == 1); + boundsInScreen.readFromParcel(parcel); + + final boolean hasChildren = (parcel.readInt() == 1); + if (hasChildren) { + if (childTokens == null) { + childTokens = new ArrayList<IBinder>(); + } + parcel.readBinderList(childTokens); + } + } + + private void clear() { + type = 0; + layer = 0; + token = null; + parentToken = null; + focused = false; + boundsInScreen.setEmpty(); + if (childTokens != null) { + childTokens.clear(); + } + } + + public static final Parcelable.Creator<WindowInfo> CREATOR = + new Creator<WindowInfo>() { + @Override + public WindowInfo createFromParcel(Parcel parcel) { + WindowInfo window = obtain(); + window.initFromParcel(parcel); + return window; + } + + @Override + public WindowInfo[] newArray(int size) { + return new WindowInfo[size]; + } + }; +} diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index d5a7d33..032a82f 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -49,7 +49,7 @@ import android.util.Log; public interface WindowManager extends ViewManager { /** * Exception that is thrown when trying to add view whose - * {@link WindowManager.LayoutParams} {@link WindowManager.LayoutParams#token} + * {@link LayoutParams} {@link LayoutParams#token} * is invalid. */ public static class BadTokenException extends RuntimeException { @@ -173,7 +173,6 @@ public interface WindowManager extends ViewManager { * @see #TYPE_SEARCH_BAR * @see #TYPE_PHONE * @see #TYPE_SYSTEM_ALERT - * @see #TYPE_KEYGUARD * @see #TYPE_TOAST * @see #TYPE_SYSTEM_OVERLAY * @see #TYPE_PRIORITY_PHONE @@ -197,7 +196,6 @@ public interface WindowManager extends ViewManager { @ViewDebug.IntToString(from = TYPE_SEARCH_BAR, to = "TYPE_SEARCH_BAR"), @ViewDebug.IntToString(from = TYPE_PHONE, to = "TYPE_PHONE"), @ViewDebug.IntToString(from = TYPE_SYSTEM_ALERT, to = "TYPE_SYSTEM_ALERT"), - @ViewDebug.IntToString(from = TYPE_KEYGUARD, to = "TYPE_KEYGUARD"), @ViewDebug.IntToString(from = TYPE_TOAST, to = "TYPE_TOAST"), @ViewDebug.IntToString(from = TYPE_SYSTEM_OVERLAY, to = "TYPE_SYSTEM_OVERLAY"), @ViewDebug.IntToString(from = TYPE_PRIORITY_PHONE, to = "TYPE_PRIORITY_PHONE"), @@ -341,13 +339,13 @@ public interface WindowManager extends ViewManager { * In multiuser systems shows only on the owning user's window. */ public static final int TYPE_SYSTEM_ALERT = FIRST_SYSTEM_WINDOW+3; - + /** * Window type: keyguard window. * In multiuser systems shows on all users' windows. */ public static final int TYPE_KEYGUARD = FIRST_SYSTEM_WINDOW+4; - + /** * Window type: transient notifications. * In multiuser systems shows only on the owning user's window. @@ -916,7 +914,6 @@ public interface WindowManager extends ViewManager { */ public static final int FLAG_NEEDS_MENU_KEY = 0x40000000; - /** * Various behavioral options/flags. Default is none. * @@ -1090,6 +1087,14 @@ public interface WindowManager extends ViewManager { public static final int PRIVATE_FLAG_INHERIT_TRANSLUCENT_DECOR = 0x00000200; /** + * Flag whether the current window is a keyguard window, meaning that it will hide all other + * windows behind it except for windows with flag {@link #FLAG_SHOW_WHEN_LOCKED} set. + * Further, this can only be set by {@link LayoutParams#TYPE_STATUS_BAR}. + * {@hide} + */ + public static final int PRIVATE_FLAG_KEYGUARD = 0x00000400; + + /** * Control flags that are private to the platform. * @hide */ diff --git a/core/java/android/view/WindowManagerInternal.java b/core/java/android/view/WindowManagerInternal.java index a1bd4bd..14dc356 100644 --- a/core/java/android/view/WindowManagerInternal.java +++ b/core/java/android/view/WindowManagerInternal.java @@ -16,7 +16,12 @@ package android.view; +import android.graphics.Rect; +import android.graphics.Region; import android.hardware.display.DisplayManagerInternal; +import android.os.IBinder; + +import java.util.List; /** * Window manager local system service interface. @@ -24,10 +29,136 @@ import android.hardware.display.DisplayManagerInternal; * @hide Only for use within the system server. */ public abstract class WindowManagerInternal { + + /** + * Interface to receive a callback when the windows reported for + * accessibility changed. + */ + public interface WindowsForAccessibilityCallback { + + /** + * Called when the windows for accessibility changed. + * + * @param windows The windows for accessibility. + */ + public void onWindowsForAccessibilityChanged(List<WindowInfo> windows); + } + + /** + * Callbacks for contextual changes that affect the screen magnification + * feature. + */ + public interface MagnificationCallbacks { + + /** + * Called when the bounds of the screen content that is magnified changed. + * Note that not the entire screen is magnified. + * + * @param bounds The bounds. + */ + public void onMagnifedBoundsChanged(Region bounds); + + /** + * Called when an application requests a rectangle on the screen to allow + * the client to apply the appropriate pan and scale. + * + * @param left The rectangle left. + * @param top The rectangle top. + * @param right The rectangle right. + * @param bottom The rectangle bottom. + */ + public void onRectangleOnScreenRequested(int left, int top, int right, int bottom); + + /** + * Notifies that the rotation changed. + * + * @param rotation The current rotation. + */ + public void onRotationChanged(int rotation); + + /** + * Notifies that the context of the user changed. For example, an application + * was started. + */ + public void onUserContextChanged(); + } + /** * Request that the window manager call * {@link DisplayManagerInternal#performTraversalInTransactionFromWindowManager} * within a surface transaction at a later time. */ public abstract void requestTraversalFromDisplayManager(); -}
\ No newline at end of file + + /** + * Set by the accessibility layer to observe changes in the magnified region, + * rotation, and other window transformations related to display magnification + * as the window manager is responsible for doing the actual magnification + * and has access to the raw window data while the accessibility layer serves + * as a controller. + * + * @param callbacks The callbacks to invoke. + */ + public abstract void setMagnificationCallbacks(MagnificationCallbacks callbacks); + + /** + * Set by the accessibility layer to specify the magnification and panning to + * be applied to all windows that should be magnified. + * + * @param callbacks The callbacks to invoke. + * + * @see #setMagnificationCallbacks(MagnificationCallbacks) + */ + public abstract void setMagnificationSpec(MagnificationSpec spec); + + /** + * Gets the magnification and translation applied to a window given its token. + * Not all windows are magnified and the window manager policy determines which + * windows are magnified. The returned result also takes into account the compat + * scale if necessary. + * + * @param windowToken The window's token. + * + * @return The magnification spec for the window. + * + * @see #setMagnificationCallbacks(MagnificationCallbacks) + */ + public abstract MagnificationSpec getCompatibleMagnificationSpecForWindow( + IBinder windowToken); + + /** + * Sets a callback for observing which windows are touchable for the purposes + * of accessibility. + * + * @param callback The callback. + */ + public abstract void setWindowsForAccessibilityCallback( + WindowsForAccessibilityCallback callback); + + /** + * Sets a filter for manipulating the input event stream. + * + * @param filter The filter implementation. + */ + public abstract void setInputFilter(IInputFilter filter); + + /** + * Gets the token of the window that has input focus. + * + * @return The token. + */ + public abstract IBinder getFocusedWindowToken(); + + /** + * @return Whether the keyguard is engaged. + */ + public abstract boolean isKeyguardLocked(); + + /** + * Gets the frame of a window given its token. + * + * @param token The token. + * @param outBounds The frame to populate. + */ + public abstract void getWindowFrame(IBinder token, Rect outBounds); +} diff --git a/core/java/android/view/WindowManagerPolicy.java b/core/java/android/view/WindowManagerPolicy.java index 893db89..c9be54d 100644 --- a/core/java/android/view/WindowManagerPolicy.java +++ b/core/java/android/view/WindowManagerPolicy.java @@ -16,7 +16,9 @@ package android.view; +import android.annotation.IntDef; import android.content.Context; +import android.content.pm.ActivityInfo; import android.content.res.CompatibilityInfo; import android.content.res.Configuration; import android.graphics.Rect; @@ -27,6 +29,8 @@ import android.os.Looper; import android.view.animation.Animation; import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * This interface supplies all UI-specific behavior of the window manager. An @@ -449,6 +453,11 @@ public interface WindowManagerPolicy { /** Screen turned off because of proximity sensor */ public final int OFF_BECAUSE_OF_PROX_SENSOR = 4; + /** @hide */ + @IntDef({USER_ROTATION_FREE, USER_ROTATION_LOCKED}) + @Retention(RetentionPolicy.SOURCE) + public @interface UserRotationMode {} + /** When not otherwise specified by the activity's screenOrientation, rotation should be * determined by the system (that is, using sensors). */ public final int USER_ROTATION_FREE = 0; @@ -996,6 +1005,14 @@ public interface WindowManagerPolicy { public void dismissKeyguardLw(); /** + * Ask the policy whether the Keyguard has drawn. If the Keyguard is disabled, this method + * returns true as soon as we know that Keyguard is disabled. + * + * @return true if the keyguard has drawn. + */ + public boolean isKeyguardDrawnLw(); + + /** * Given an orientation constant, returns the appropriate surface rotation, * taking into account sensors, docking mode, rotation lock, and other factors. * @@ -1004,7 +1021,8 @@ public interface WindowManagerPolicy { * @param lastRotation The most recently used rotation. * @return The surface rotation to use. */ - public int rotationForOrientationLw(int orientation, int lastRotation); + public int rotationForOrientationLw(@ActivityInfo.ScreenOrientation int orientation, + int lastRotation); /** * Given an orientation constant and a rotation, returns true if the rotation @@ -1019,7 +1037,8 @@ public interface WindowManagerPolicy { * @param rotation The rotation to check. * @return True if the rotation is compatible with the requested orientation. */ - public boolean rotationHasCompatibleMetricsLw(int orientation, int rotation); + public boolean rotationHasCompatibleMetricsLw(@ActivityInfo.ScreenOrientation int orientation, + int rotation); /** * Called by the window manager when the rotation changes. @@ -1068,7 +1087,7 @@ public interface WindowManagerPolicy { */ public void enableScreenAfterBoot(); - public void setCurrentOrientationLw(int newOrientation); + public void setCurrentOrientationLw(@ActivityInfo.ScreenOrientation int newOrientation); /** * Call from application to perform haptic feedback on its window. @@ -1095,6 +1114,7 @@ public interface WindowManagerPolicy { * @see WindowManagerPolicy#USER_ROTATION_LOCKED * @see WindowManagerPolicy#USER_ROTATION_FREE */ + @UserRotationMode public int getUserRotationMode(); /** @@ -1105,12 +1125,12 @@ public interface WindowManagerPolicy { * @param rotation One of {@link Surface#ROTATION_0}, {@link Surface#ROTATION_90}, * {@link Surface#ROTATION_180}, {@link Surface#ROTATION_270}. */ - public void setUserRotationMode(int mode, int rotation); + public void setUserRotationMode(@UserRotationMode int mode, @Surface.Rotation int rotation); /** * Called when a new system UI visibility is being reported, allowing * the policy to adjust what is actually reported. - * @param visibility The raw visiblity reported by the status bar. + * @param visibility The raw visibility reported by the status bar. * @return The new desired visibility. */ public int adjustSystemUiVisibilityLw(int visibility); @@ -1133,6 +1153,11 @@ public interface WindowManagerPolicy { public void setLastInputMethodWindowLw(WindowState ime, WindowState target); /** + * @return The current height of the input method window. + */ + public int getInputMethodWindowVisibleHeightLw(); + + /** * Called when the current user changes. Guaranteed to be called before the broadcast * of the new user id is made to all listeners. * @@ -1168,11 +1193,4 @@ public interface WindowManagerPolicy { * @return True if the window is a top level one. */ public boolean isTopLevelWindow(int windowType); - - /** - * Sets the current touch exploration state. - * - * @param enabled Whether touch exploration is enabled. - */ - public void setTouchExplorationEnabled(boolean enabled); } diff --git a/core/java/android/view/accessibility/AccessibilityCache.java b/core/java/android/view/accessibility/AccessibilityCache.java new file mode 100644 index 0000000..77d48e2 --- /dev/null +++ b/core/java/android/view/accessibility/AccessibilityCache.java @@ -0,0 +1,467 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.view.accessibility; + +import android.os.Build; +import android.util.ArraySet; +import android.util.Log; +import android.util.LongArray; +import android.util.LongSparseArray; +import android.util.SparseArray; + +import java.util.ArrayList; +import java.util.List; + +/** + * Cache for AccessibilityWindowInfos and AccessibilityNodeInfos. + * It is updated when windows change or nodes change. + */ +final class AccessibilityCache { + + private static final String LOG_TAG = "AccessibilityCache"; + + private static final boolean DEBUG = false; + + private static final boolean CHECK_INTEGRITY = Build.IS_DEBUGGABLE; + + private final Object mLock = new Object(); + + private final LongArray mTempLongArray = new LongArray(); + + private final SparseArray<AccessibilityWindowInfo> mWindowCache = + new SparseArray<AccessibilityWindowInfo>(); + + private final SparseArray<LongSparseArray<AccessibilityNodeInfo>> mNodeCache = + new SparseArray<LongSparseArray<AccessibilityNodeInfo>>(); + + private final SparseArray<AccessibilityWindowInfo> mTempWindowArray = + new SparseArray<AccessibilityWindowInfo>(); + + public void addWindow(AccessibilityWindowInfo window) { + synchronized (mLock) { + if (DEBUG) { + Log.i(LOG_TAG, "Caching window: " + window.getId()); + } + mWindowCache.put(window.getId(), window); + } + } + + public void removeWindows(int[] windowIds) { + synchronized (mLock) { + final int windowCount = windowIds.length; + for (int i = 0; i < windowCount; i++) { + final int windowId = windowIds[i]; + AccessibilityWindowInfo window = mWindowCache.get(windowId); + if (window != null) { + if (DEBUG) { + Log.i(LOG_TAG, "Removing window: " + windowId); + } + window.recycle(); + mWindowCache.remove(windowId); + } + clearNodesForWindowLocked(windowIds[i]); + } + } + } + + /** + * Notifies the cache that the something in the UI changed. As a result + * the cache will either refresh some nodes or evict some nodes. + * + * @param event An event. + */ + public void onAccessibilityEvent(AccessibilityEvent event) { + synchronized (mLock) { + final int eventType = event.getEventType(); + switch (eventType) { + case AccessibilityEvent.TYPE_VIEW_FOCUSED: + case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED: + case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED: + case AccessibilityEvent.TYPE_VIEW_SELECTED: + case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED: + case AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED: { + refreshCachedNodeLocked(event.getWindowId(), event.getSourceNodeId()); + } break; + + case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: { + synchronized (mLock) { + final int windowId = event.getWindowId(); + final long sourceId = event.getSourceNodeId(); + if ((event.getContentChangeTypes() + & AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE) != 0) { + clearSubTreeLocked(windowId, sourceId); + } else { + refreshCachedNodeLocked(windowId, sourceId); + } + } + } break; + + case AccessibilityEvent.TYPE_VIEW_SCROLLED: { + clearSubTreeLocked(event.getWindowId(), event.getSourceNodeId()); + } break; + } + } + + if (CHECK_INTEGRITY) { + checkIntegrity(); + } + } + + private void refreshCachedNodeLocked(int windowId, long sourceId) { + if (DEBUG) { + Log.i(LOG_TAG, "Refreshing cached node."); + } + + LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId); + if (nodes == null) { + return; + } + AccessibilityNodeInfo cachedInfo = nodes.get(sourceId); + // If the source is not in the cache - nothing to do. + if (cachedInfo == null) { + return; + } + // The node changed so we will just refresh it right now. + if (cachedInfo.refresh(true)) { + return; + } + // Weird, we could not refresh. Just evict the entire sub-tree. + clearSubTreeLocked(windowId, sourceId); + } + + /** + * Gets a cached {@link AccessibilityNodeInfo} given the id of the hosting + * window and the accessibility id of the node. + * + * @param windowId The id of the window hosting the node. + * @param accessibilityNodeId The info accessibility node id. + * @return The cached {@link AccessibilityNodeInfo} or null if such not found. + */ + public AccessibilityNodeInfo getNode(int windowId, long accessibilityNodeId) { + synchronized(mLock) { + LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId); + if (nodes == null) { + return null; + } + AccessibilityNodeInfo info = nodes.get(accessibilityNodeId); + if (info != null) { + // Return a copy since the client calls to AccessibilityNodeInfo#recycle() + // will wipe the data of the cached info. + info = AccessibilityNodeInfo.obtain(info); + } + if (DEBUG) { + Log.i(LOG_TAG, "get(" + accessibilityNodeId + ") = " + info); + } + return info; + } + } + + public List<AccessibilityWindowInfo> getWindows() { + synchronized (mLock) { + final int windowCount = mWindowCache.size(); + if (windowCount > 0) { + // Careful to return the windows in a decreasing layer order. + SparseArray<AccessibilityWindowInfo> sortedWindows = mTempWindowArray; + sortedWindows.clear(); + + for (int i = 0; i < windowCount; i++) { + AccessibilityWindowInfo window = mWindowCache.valueAt(i); + sortedWindows.put(window.getLayer(), window); + } + + List<AccessibilityWindowInfo> windows = new ArrayList<AccessibilityWindowInfo>(); + for (int i = windowCount - 1; i >= 0; i--) { + AccessibilityWindowInfo window = sortedWindows.valueAt(i); + windows.add(AccessibilityWindowInfo.obtain(window)); + } + + sortedWindows.clear(); + + return windows; + } + return null; + } + } + + public AccessibilityWindowInfo getWindow(int windowId) { + synchronized (mLock) { + AccessibilityWindowInfo window = mWindowCache.get(windowId); + if (window != null) { + return AccessibilityWindowInfo.obtain(window); + } + return null; + } + } + + /** + * Caches an {@link AccessibilityNodeInfo}. + * + * @param info The node to cache. + */ + public void add(AccessibilityNodeInfo info) { + synchronized(mLock) { + if (DEBUG) { + Log.i(LOG_TAG, "add(" + info + ")"); + } + + final int windowId = info.getWindowId(); + LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId); + if (nodes == null) { + nodes = new LongSparseArray<AccessibilityNodeInfo>(); + mNodeCache.put(windowId, nodes); + } + + final long sourceId = info.getSourceNodeId(); + AccessibilityNodeInfo oldInfo = nodes.get(sourceId); + if (oldInfo != null) { + // If the added node is in the cache we have to be careful if + // the new one represents a source state where some of the + // children have been removed to remove the descendants that + // are no longer present. + final LongArray newChildrenIds = info.getChildNodeIds(); + if (newChildrenIds != null) { + // Cache the new ids as we will do some lookups. + LongArray newChildNodeIds = mTempLongArray; + final int newChildCount = newChildNodeIds.size(); + for (int i = 0; i < newChildCount; i++) { + newChildNodeIds.add(newChildrenIds.get(i)); + } + + final int oldChildCount = oldInfo.getChildCount(); + for (int i = 0; i < oldChildCount; i++) { + final long oldChildId = oldInfo.getChildId(i); + if (newChildNodeIds.indexOf(oldChildId) < 0) { + clearSubTreeLocked(windowId, oldChildId); + } + } + + newChildNodeIds.clear(); + } + + // Also be careful if the parent has changed since the new + // parent may be a predecessor of the old parent which will + // add cyclse to the cache. + final long oldParentId = oldInfo.getParentNodeId(); + if (info.getParentNodeId() != oldParentId) { + clearSubTreeLocked(windowId, oldParentId); + } + } + + // Cache a copy since the client calls to AccessibilityNodeInfo#recycle() + // will wipe the data of the cached info. + AccessibilityNodeInfo clone = AccessibilityNodeInfo.obtain(info); + nodes.put(sourceId, clone); + } + } + + /** + * Clears the cache. + */ + public void clear() { + synchronized(mLock) { + if (DEBUG) { + Log.i(LOG_TAG, "clear()"); + } + final int windowCount = mWindowCache.size(); + for (int i = windowCount - 1; i >= 0; i--) { + AccessibilityWindowInfo window = mWindowCache.valueAt(i); + window.recycle(); + mWindowCache.removeAt(i); + } + final int nodesForWindowCount = mNodeCache.size(); + for (int i = 0; i < nodesForWindowCount; i++) { + final int windowId = mNodeCache.keyAt(i); + clearNodesForWindowLocked(windowId); + } + } + } + + private void clearNodesForWindowLocked(int windowId) { + if (DEBUG) { + Log.i(LOG_TAG, "clearWindowLocked(" + windowId + ")"); + } + LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId); + if (nodes == null) { + return; + } + // Recycle the nodes before clearing the cache. + final int nodeCount = nodes.size(); + for (int i = nodeCount - 1; i >= 0; i--) { + AccessibilityNodeInfo info = nodes.valueAt(i); + nodes.removeAt(i); + info.recycle(); + } + mNodeCache.remove(windowId); + } + + /** + * Clears a subtree rooted at the node with the given id that is + * hosted in a given window. + * + * @param windowId The id of the hosting window. + * @param rootNodeId The root id. + */ + private void clearSubTreeLocked(int windowId, long rootNodeId) { + if (DEBUG) { + Log.i(LOG_TAG, "Clearing cached subtree."); + } + LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId); + if (nodes != null) { + clearSubTreeRecursiveLocked(nodes, rootNodeId); + } + } + + /** + * Clears a subtree given a pointer to the root id and the nodes + * in the hosting window. + * + * @param nodes The nodes in the hosting window. + * @param rootNodeId The id of the root to evict. + */ + private void clearSubTreeRecursiveLocked(LongSparseArray<AccessibilityNodeInfo> nodes, + long rootNodeId) { + AccessibilityNodeInfo current = nodes.get(rootNodeId); + if (current == null) { + return; + } + nodes.remove(rootNodeId); + final int childCount = current.getChildCount(); + for (int i = 0; i < childCount; i++) { + final long childNodeId = current.getChildId(i); + clearSubTreeRecursiveLocked(nodes, childNodeId); + } + } + + /** + * Check the integrity of the cache which is nodes from different windows + * are not mixed, there is a single active window, there is a single focused + * window, for every window there are no duplicates nodes, all nodes for a + * window are connected, for every window there is a single input focused + * node, and for every window there is a single accessibility focused node. + */ + public void checkIntegrity() { + synchronized (mLock) { + // Get the root. + if (mWindowCache.size() <= 0 && mNodeCache.size() == 0) { + return; + } + + AccessibilityWindowInfo focusedWindow = null; + AccessibilityWindowInfo activeWindow = null; + + final int windowCount = mWindowCache.size(); + for (int i = 0; i < windowCount; i++) { + AccessibilityWindowInfo window = mWindowCache.valueAt(i); + + // Check for one active window. + if (window.isActive()) { + if (activeWindow != null) { + Log.e(LOG_TAG, "Duplicate active window:" + window); + } else { + activeWindow = window; + } + } + + // Check for one focused window. + if (window.isFocused()) { + if (focusedWindow != null) { + Log.e(LOG_TAG, "Duplicate focused window:" + window); + } else { + focusedWindow = window; + } + } + } + + // Traverse the tree and do some checks. + AccessibilityNodeInfo accessFocus = null; + AccessibilityNodeInfo inputFocus = null; + + final int nodesForWindowCount = mNodeCache.size(); + for (int i = 0; i < nodesForWindowCount; i++) { + LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.valueAt(i); + if (nodes.size() <= 0) { + continue; + } + + ArraySet<AccessibilityNodeInfo> seen = new ArraySet<AccessibilityNodeInfo>(); + final int windowId = mNodeCache.keyAt(i); + + final int nodeCount = nodes.size(); + for (int j = 0; j < nodeCount; j++) { + AccessibilityNodeInfo node = nodes.valueAt(j); + + // Check for duplicates + if (!seen.add(node)) { + Log.e(LOG_TAG, "Duplicate node: " + node + + " in window:" + windowId); + } + + // Check for one accessibility focus. + if (node.isAccessibilityFocused()) { + if (accessFocus != null) { + Log.e(LOG_TAG, "Duplicate accessibility focus:" + node + + " in window:" + windowId); + } else { + accessFocus = node; + } + } + + // Check for one input focus. + if (node.isFocused()) { + if (inputFocus != null) { + Log.e(LOG_TAG, "Duplicate input focus: " + node + + " in window:" + windowId); + } else { + inputFocus = node; + } + } + + // The node should be a child of its parent if we have the parent. + AccessibilityNodeInfo nodeParent = nodes.get(node.getParentNodeId()); + if (nodeParent != null) { + boolean childOfItsParent = false; + final int childCount = nodeParent.getChildCount(); + for (int k = 0; k < childCount; k++) { + AccessibilityNodeInfo child = nodes.get(nodeParent.getChildId(k)); + if (child == node) { + childOfItsParent = true; + break; + } + } + if (!childOfItsParent) { + Log.e(LOG_TAG, "Invalid parent-child ralation between parent: " + + nodeParent + " and child: " + node); + } + } + + // The node should be the parent of its child if we have the child. + final int childCount = node.getChildCount(); + for (int k = 0; k < childCount; k++) { + AccessibilityNodeInfo child = nodes.get(node.getChildId(k)); + if (child != null) { + AccessibilityNodeInfo parent = nodes.get(child.getParentNodeId()); + if (parent != node) { + Log.e(LOG_TAG, "Invalid child-parent ralation between child: " + + node + " and parent: " + nodeParent); + } + } + } + } + } + } + } +} diff --git a/core/java/android/view/accessibility/AccessibilityEvent.java b/core/java/android/view/accessibility/AccessibilityEvent.java index f635eee..417e22c 100644 --- a/core/java/android/view/accessibility/AccessibilityEvent.java +++ b/core/java/android/view/accessibility/AccessibilityEvent.java @@ -343,6 +343,23 @@ import java.util.List; * view.</br> * </p> * <p> + * <b>Windows changed</b> - represents the event of changes in the windows shown on + * the screen such as a window appeared, a window disappeared, a window size changed, + * a window layer changed, etc.</br> + * <em>Type:</em> {@link #TYPE_WINDOWS_CHANGED}</br> + * <em>Properties:</em></br> + * <ul> + * <li>{@link #getEventType()} - The type of the event.</li> + * <li>{@link #getEventTime()} - The event time.</li> + * </ul> + * <em>Note:</em> You can retrieve the {@link AccessibilityWindowInfo} for the window + * source of the event via {@link AccessibilityEvent#getSource()} to get the source + * node on which then call {@link AccessibilityNodeInfo#getWindow() + * AccessibilityNodeInfo.getWindow()} to get the window. Also all windows on the screen can + * be retrieved by a call to {@link android.accessibilityservice.AccessibilityService#getWindows() + * android.accessibilityservice.AccessibilityService.getWindows()}. + * </p> + * <p> * <b>NOTIFICATION TYPES</b></br> * </p> * <p> @@ -662,6 +679,11 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par public static final int TYPE_TOUCH_INTERACTION_END = 0x00200000; /** + * Represents the event change in the windows shown on the screen. + */ + public static final int TYPE_WINDOWS_CHANGED = 0x00400000; + + /** * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event: * The type of change is not defined. */ @@ -708,6 +730,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par * @see #TYPE_GESTURE_DETECTION_END * @see #TYPE_TOUCH_INTERACTION_START * @see #TYPE_TOUCH_INTERACTION_END + * @see #TYPE_WINDOWS_CHANGED */ public static final int TYPES_ALL_MASK = 0xFFFFFFFF; @@ -722,7 +745,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par int mAction; int mContentChangeTypes; - private final ArrayList<AccessibilityRecord> mRecords = new ArrayList<AccessibilityRecord>(); + private ArrayList<AccessibilityRecord> mRecords; /* * Hide constructor from clients. @@ -755,11 +778,13 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par @Override public void setSealed(boolean sealed) { super.setSealed(sealed); - List<AccessibilityRecord> records = mRecords; - final int recordCount = records.size(); - for (int i = 0; i < recordCount; i++) { - AccessibilityRecord record = records.get(i); - record.setSealed(sealed); + final List<AccessibilityRecord> records = mRecords; + if (records != null) { + final int recordCount = records.size(); + for (int i = 0; i < recordCount; i++) { + AccessibilityRecord record = records.get(i); + record.setSealed(sealed); + } } } @@ -769,7 +794,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par * @return The number of records. */ public int getRecordCount() { - return mRecords.size(); + return mRecords == null ? 0 : mRecords.size(); } /** @@ -781,6 +806,9 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par */ public void appendRecord(AccessibilityRecord record) { enforceNotSealed(); + if (mRecords == null) { + mRecords = new ArrayList<AccessibilityRecord>(); + } mRecords.add(record); } @@ -791,6 +819,9 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par * @return The record at the specified index. */ public AccessibilityRecord getRecord(int index) { + if (mRecords == null) { + throw new IndexOutOfBoundsException("Invalid index " + index + ", size is 0"); + } return mRecords.get(index); } @@ -964,11 +995,14 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par AccessibilityEvent eventClone = AccessibilityEvent.obtain(); eventClone.init(event); - final int recordCount = event.mRecords.size(); - for (int i = 0; i < recordCount; i++) { - AccessibilityRecord record = event.mRecords.get(i); - AccessibilityRecord recordClone = AccessibilityRecord.obtain(record); - eventClone.mRecords.add(recordClone); + if (event.mRecords != null) { + final int recordCount = event.mRecords.size(); + eventClone.mRecords = new ArrayList<AccessibilityRecord>(recordCount); + for (int i = 0; i < recordCount; i++) { + final AccessibilityRecord record = event.mRecords.get(i); + final AccessibilityRecord recordClone = AccessibilityRecord.obtain(record); + eventClone.mRecords.add(recordClone); + } } return eventClone; @@ -1013,9 +1047,11 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par mContentChangeTypes = 0; mPackageName = null; mEventTime = 0; - while (!mRecords.isEmpty()) { - AccessibilityRecord record = mRecords.remove(0); - record.recycle(); + if (mRecords != null) { + while (!mRecords.isEmpty()) { + AccessibilityRecord record = mRecords.remove(0); + record.recycle(); + } } } @@ -1037,11 +1073,14 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par // Read the records. final int recordCount = parcel.readInt(); - for (int i = 0; i < recordCount; i++) { - AccessibilityRecord record = AccessibilityRecord.obtain(); - readAccessibilityRecordFromParcel(record, parcel); - record.mConnectionId = mConnectionId; - mRecords.add(record); + if (recordCount > 0) { + mRecords = new ArrayList<AccessibilityRecord>(recordCount); + for (int i = 0; i < recordCount; i++) { + AccessibilityRecord record = AccessibilityRecord.obtain(); + readAccessibilityRecordFromParcel(record, parcel); + record.mConnectionId = mConnectionId; + mRecords.add(record); + } } } @@ -1147,8 +1186,8 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par builder.append("; ContentChangeTypes: ").append(mContentChangeTypes); builder.append("; sourceWindowId: ").append(mSourceWindowId); builder.append("; mSourceNodeId: ").append(mSourceNodeId); - for (int i = 0; i < mRecords.size(); i++) { - AccessibilityRecord record = mRecords.get(i); + for (int i = 0; i < getRecordCount(); i++) { + final AccessibilityRecord record = getRecord(i); builder.append(" Record "); builder.append(i); builder.append(":"); @@ -1350,6 +1389,13 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par builder.append("TYPE_TOUCH_INTERACTION_END"); eventTypeCount++; } break; + case TYPE_WINDOWS_CHANGED: { + if (eventTypeCount > 0) { + builder.append(", "); + } + builder.append("TYPE_WINDOWS_CHANGED"); + eventTypeCount++; + } break; } } if (eventTypeCount > 1) { diff --git a/core/java/android/view/accessibility/AccessibilityInteractionClient.java b/core/java/android/view/accessibility/AccessibilityInteractionClient.java index 139df3e..5b9372d 100644 --- a/core/java/android/view/accessibility/AccessibilityInteractionClient.java +++ b/core/java/android/view/accessibility/AccessibilityInteractionClient.java @@ -27,7 +27,6 @@ import android.os.SystemClock; import android.util.Log; import android.util.LongSparseArray; import android.util.SparseArray; -import android.util.SparseLongArray; import java.util.ArrayList; import java.util.Collections; @@ -101,14 +100,11 @@ public final class AccessibilityInteractionClient private Message mSameThreadMessage; - // The connection cache is shared between all interrogating threads. private static final SparseArray<IAccessibilityServiceConnection> sConnectionCache = new SparseArray<IAccessibilityServiceConnection>(); - // The connection cache is shared between all interrogating threads since - // at any given time there is only one window allowing querying. - private static final AccessibilityNodeInfoCache sAccessibilityNodeInfoCache = - new AccessibilityNodeInfoCache(); + private static final AccessibilityCache sAccessibilityCache = + new AccessibilityCache(); /** * @return The client for the current thread. @@ -167,6 +163,99 @@ public final class AccessibilityInteractionClient } /** + * Gets the root {@link AccessibilityNodeInfo} in a given window. + * + * @param connectionId The id of a connection for interacting with the system. + * @param windowId The window id. + * @return The root {@link AccessibilityNodeInfo} if found, null otherwise. + */ + public AccessibilityNodeInfo getRootInWindow(int connectionId, int windowId) { + return findAccessibilityNodeInfoByAccessibilityId(connectionId, windowId, + AccessibilityNodeInfo.ROOT_NODE_ID, false, + AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS); + } + + /** + * Gets the info for a window. + * + * @param connectionId The id of a connection for interacting with the system. + * @param accessibilityWindowId A unique window id. Use + * {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID} + * to query the currently active window. + * @return The {@link AccessibilityWindowInfo}. + */ + public AccessibilityWindowInfo getWindow(int connectionId, int accessibilityWindowId) { + try { + IAccessibilityServiceConnection connection = getConnection(connectionId); + if (connection != null) { + AccessibilityWindowInfo window = sAccessibilityCache.getWindow( + accessibilityWindowId); + if (window != null) { + if (DEBUG) { + Log.i(LOG_TAG, "Window cache hit"); + } + return window; + } + if (DEBUG) { + Log.i(LOG_TAG, "Window cache miss"); + } + window = connection.getWindow(accessibilityWindowId); + if (window != null) { + sAccessibilityCache.addWindow(window); + return window; + } + } else { + if (DEBUG) { + Log.w(LOG_TAG, "No connection for connection id: " + connectionId); + } + } + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while calling remote getWindow", re); + } + return null; + } + + /** + * Gets the info for all windows. + * + * @param connectionId The id of a connection for interacting with the system. + * @return The {@link AccessibilityWindowInfo} list. + */ + public List<AccessibilityWindowInfo> getWindows(int connectionId) { + try { + IAccessibilityServiceConnection connection = getConnection(connectionId); + if (connection != null) { + List<AccessibilityWindowInfo> windows = sAccessibilityCache.getWindows(); + if (windows != null) { + if (DEBUG) { + Log.i(LOG_TAG, "Window cache hit"); + } + return windows; + } + if (DEBUG) { + Log.i(LOG_TAG, "Window cache miss"); + } + windows = connection.getWindows(); + if (windows != null) { + final int windowCount = windows.size(); + for (int i = 0; i < windowCount; i++) { + AccessibilityWindowInfo window = windows.get(i); + sAccessibilityCache.addWindow(window); + } + return windows; + } + } else { + if (DEBUG) { + Log.w(LOG_TAG, "No connection for connection id: " + connectionId); + } + } + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while calling remote getWindows", re); + } + return Collections.emptyList(); + } + + /** * Finds an {@link AccessibilityNodeInfo} by accessibility id. * * @param connectionId The id of a connection for interacting with the system. @@ -184,15 +273,26 @@ public final class AccessibilityInteractionClient public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(int connectionId, int accessibilityWindowId, long accessibilityNodeId, boolean bypassCache, int prefetchFlags) { + if ((prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS) != 0 + && (prefetchFlags & AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS) == 0) { + throw new IllegalArgumentException("FLAG_PREFETCH_SIBLINGS" + + " requires FLAG_PREFETCH_PREDECESSORS"); + } try { IAccessibilityServiceConnection connection = getConnection(connectionId); if (connection != null) { if (!bypassCache) { - AccessibilityNodeInfo cachedInfo = sAccessibilityNodeInfoCache.get( - accessibilityNodeId); + AccessibilityNodeInfo cachedInfo = sAccessibilityCache.getNode( + accessibilityWindowId, accessibilityNodeId); if (cachedInfo != null) { + if (DEBUG) { + Log.i(LOG_TAG, "Node cache hit"); + } return cachedInfo; } + if (DEBUG) { + Log.i(LOG_TAG, "Node cache miss"); + } } final int interactionId = mInteractionIdCounter.getAndIncrement(); final boolean success = connection.findAccessibilityNodeInfoByAccessibilityId( @@ -213,10 +313,8 @@ public final class AccessibilityInteractionClient } } } catch (RemoteException re) { - if (DEBUG) { - Log.w(LOG_TAG, "Error while calling remote" - + " findAccessibilityNodeInfoByAccessibilityId", re); - } + Log.e(LOG_TAG, "Error while calling remote" + + " findAccessibilityNodeInfoByAccessibilityId", re); } return null; } @@ -260,10 +358,8 @@ public final class AccessibilityInteractionClient } } } catch (RemoteException re) { - if (DEBUG) { - Log.w(LOG_TAG, "Error while calling remote" - + " findAccessibilityNodeInfoByViewIdInActiveWindow", re); - } + Log.w(LOG_TAG, "Error while calling remote" + + " findAccessibilityNodeInfoByViewIdInActiveWindow", re); } return Collections.emptyList(); } @@ -308,10 +404,8 @@ public final class AccessibilityInteractionClient } } } catch (RemoteException re) { - if (DEBUG) { - Log.w(LOG_TAG, "Error while calling remote" - + " findAccessibilityNodeInfosByViewText", re); - } + Log.w(LOG_TAG, "Error while calling remote" + + " findAccessibilityNodeInfosByViewText", re); } return Collections.emptyList(); } @@ -353,9 +447,7 @@ public final class AccessibilityInteractionClient } } } catch (RemoteException re) { - if (DEBUG) { - Log.w(LOG_TAG, "Error while calling remote findFocus", re); - } + Log.w(LOG_TAG, "Error while calling remote findFocus", re); } return null; } @@ -397,9 +489,7 @@ public final class AccessibilityInteractionClient } } } catch (RemoteException re) { - if (DEBUG) { - Log.w(LOG_TAG, "Error while calling remote accessibilityFocusSearch", re); - } + Log.w(LOG_TAG, "Error while calling remote accessibilityFocusSearch", re); } return null; } @@ -437,19 +527,21 @@ public final class AccessibilityInteractionClient } } } catch (RemoteException re) { - if (DEBUG) { - Log.w(LOG_TAG, "Error while calling remote performAccessibilityAction", re); - } + Log.w(LOG_TAG, "Error while calling remote performAccessibilityAction", re); } return false; } public void clearCache() { - sAccessibilityNodeInfoCache.clear(); + sAccessibilityCache.clear(); } public void onAccessibilityEvent(AccessibilityEvent event) { - sAccessibilityNodeInfoCache.onAccessibilityEvent(event); + sAccessibilityCache.onAccessibilityEvent(event); + } + + public void removeWindows(int[] windowIds) { + sAccessibilityCache.removeWindows(windowIds); } /** @@ -614,7 +706,7 @@ public final class AccessibilityInteractionClient if (info != null) { info.setConnectionId(connectionId); info.setSealed(true); - sAccessibilityNodeInfoCache.add(info); + sAccessibilityCache.add(info); } } @@ -718,10 +810,9 @@ public final class AccessibilityInteractionClient Log.e(LOG_TAG, "Duplicate node."); return; } - SparseLongArray childIds = current.getChildNodeIds(); - final int childCount = childIds.size(); + final int childCount = current.getChildCount(); for (int i = 0; i < childCount; i++) { - final long childId = childIds.valueAt(i); + final long childId = current.getChildId(i); for (int j = 0; j < infoCount; j++) { AccessibilityNodeInfo child = infos.get(j); if (child.getSourceNodeId() == childId) { diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java index 879e58f..cbc38c6 100644 --- a/core/java/android/view/accessibility/AccessibilityManager.java +++ b/core/java/android/view/accessibility/AccessibilityManager.java @@ -75,13 +75,49 @@ public final class AccessibilityManager { /** @hide */ public static final int STATE_FLAG_TOUCH_EXPLORATION_ENABLED = 0x00000002; + /** @hide */ + public static final int INVERSION_DISABLED = -1; + + /** @hide */ + public static final int INVERSION_STANDARD = 0; + + /** @hide */ + public static final int INVERSION_HUE_ONLY = 1; + + /** @hide */ + public static final int INVERSION_VALUE_ONLY = 2; + + /** @hide */ + public static final int DALTONIZER_DISABLED = -1; + + /** @hide */ + public static final int DALTONIZER_SIMULATE_MONOCHROMACY = 0; + + /** @hide */ + public static final int DALTONIZER_SIMULATE_PROTANOMALY = 1; + + /** @hide */ + public static final int DALTONIZER_SIMULATE_DEUTERANOMALY = 2; + + /** @hide */ + public static final int DALTONIZER_SIMULATE_TRITANOMALY = 3; + + /** @hide */ + public static final int DALTONIZER_CORRECT_PROTANOMALY = 11; + + /** @hide */ + public static final int DALTONIZER_CORRECT_DEUTERANOMALY = 12; + + /** @hide */ + public static final int DALTONIZER_CORRECT_TRITANOMALY = 13; + static final Object sInstanceSync = new Object(); private static AccessibilityManager sInstance; - private static final int DO_SET_STATE = 10; + private final Object mLock = new Object(); - final IAccessibilityManager mService; + private IAccessibilityManager mService; final int mUserId; @@ -130,29 +166,14 @@ public final class AccessibilityManager { public void onTouchExplorationStateChanged(boolean enabled); } - final IAccessibilityManagerClient.Stub mClient = new IAccessibilityManagerClient.Stub() { + private final IAccessibilityManagerClient.Stub mClient = + new IAccessibilityManagerClient.Stub() { public void setState(int state) { - mHandler.obtainMessage(DO_SET_STATE, state, 0).sendToTarget(); - } - }; - - class MyHandler extends Handler { - - MyHandler(Looper mainLooper) { - super(mainLooper); - } - - @Override - public void handleMessage(Message message) { - switch (message.what) { - case DO_SET_STATE : - setState(message.arg1); - return; - default : - Log.w(LOG_TAG, "Unknown message type: " + message.what); + synchronized (mLock) { + setStateLocked(state); } } - } + }; /** * Get an AccessibilityManager instance (create one if necessary). @@ -198,26 +219,29 @@ public final class AccessibilityManager { mHandler = new MyHandler(context.getMainLooper()); mService = service; mUserId = userId; - if (mService == null) { - mIsEnabled = false; - } - try { - if (mService != null) { - final int stateFlags = mService.addClient(mClient, userId); - setState(stateFlags); - } - } catch (RemoteException re) { - Log.e(LOG_TAG, "AccessibilityManagerService is dead", re); + synchronized (mLock) { + tryConnectToServiceLocked(); } } /** + * @hide + */ + public IAccessibilityManagerClient getClient() { + return mClient; + } + + /** * Returns if the accessibility in the system is enabled. * * @return True if accessibility is enabled, false otherwise. */ public boolean isEnabled() { - synchronized (mHandler) { + synchronized (mLock) { + IAccessibilityManager service = getServiceLocked(); + if (service == null) { + return false; + } return mIsEnabled; } } @@ -228,24 +252,16 @@ public final class AccessibilityManager { * @return True if touch exploration is enabled, false otherwise. */ public boolean isTouchExplorationEnabled() { - synchronized (mHandler) { + synchronized (mLock) { + IAccessibilityManager service = getServiceLocked(); + if (service == null) { + return false; + } return mIsTouchExplorationEnabled; } } /** - * Returns the client interface this instance registers in - * the centralized accessibility manager service. - * - * @return The client. - * - * @hide - */ - public IAccessibilityManagerClient getClient() { - return (IAccessibilityManagerClient) mClient.asBinder(); - } - - /** * Sends an {@link AccessibilityEvent}. * * @param event The event to send. @@ -259,8 +275,17 @@ public final class AccessibilityManager { * their descendants. */ public void sendAccessibilityEvent(AccessibilityEvent event) { - if (!mIsEnabled) { - throw new IllegalStateException("Accessibility off. Did you forget to check that?"); + final IAccessibilityManager service; + final int userId; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return; + } + if (!mIsEnabled) { + throw new IllegalStateException("Accessibility off. Did you forget to check that?"); + } + userId = mUserId; } boolean doRecycle = false; try { @@ -269,7 +294,7 @@ public final class AccessibilityManager { // client using it is called through Binder from another process. Example: MMS // app adds a SMS notification and the NotificationManagerService calls this method long identityToken = Binder.clearCallingIdentity(); - doRecycle = mService.sendAccessibilityEvent(event, mUserId); + doRecycle = service.sendAccessibilityEvent(event, userId); Binder.restoreCallingIdentity(identityToken); if (DEBUG) { Log.i(LOG_TAG, event + " sent"); @@ -287,11 +312,20 @@ public final class AccessibilityManager { * Requests feedback interruption from all accessibility services. */ public void interrupt() { - if (!mIsEnabled) { - throw new IllegalStateException("Accessibility off. Did you forget to check that?"); + final IAccessibilityManager service; + final int userId; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return; + } + if (!mIsEnabled) { + throw new IllegalStateException("Accessibility off. Did you forget to check that?"); + } + userId = mUserId; } try { - mService.interrupt(mUserId); + service.interrupt(userId); if (DEBUG) { Log.i(LOG_TAG, "Requested interrupt from all services"); } @@ -325,18 +359,30 @@ public final class AccessibilityManager { * @return An unmodifiable list with {@link AccessibilityServiceInfo}s. */ public List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList() { + final IAccessibilityManager service; + final int userId; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return Collections.emptyList(); + } + userId = mUserId; + } + List<AccessibilityServiceInfo> services = null; try { - if (mService != null) { - services = mService.getInstalledAccessibilityServiceList(mUserId); - if (DEBUG) { - Log.i(LOG_TAG, "Installed AccessibilityServices " + services); - } + services = service.getInstalledAccessibilityServiceList(userId); + if (DEBUG) { + Log.i(LOG_TAG, "Installed AccessibilityServices " + services); } } catch (RemoteException re) { Log.e(LOG_TAG, "Error while obtaining the installed AccessibilityServices. ", re); } - return services != null ? Collections.unmodifiableList(services) : Collections.EMPTY_LIST; + if (services != null) { + return Collections.unmodifiableList(services); + } else { + return Collections.emptyList(); + } } /** @@ -354,18 +400,30 @@ public final class AccessibilityManager { */ public List<AccessibilityServiceInfo> getEnabledAccessibilityServiceList( int feedbackTypeFlags) { + final IAccessibilityManager service; + final int userId; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return Collections.emptyList(); + } + userId = mUserId; + } + List<AccessibilityServiceInfo> services = null; try { - if (mService != null) { - services = mService.getEnabledAccessibilityServiceList(feedbackTypeFlags, mUserId); - if (DEBUG) { - Log.i(LOG_TAG, "Installed AccessibilityServices " + services); - } + services = service.getEnabledAccessibilityServiceList(feedbackTypeFlags, userId); + if (DEBUG) { + Log.i(LOG_TAG, "Installed AccessibilityServices " + services); } } catch (RemoteException re) { Log.e(LOG_TAG, "Error while obtaining the installed AccessibilityServices. ", re); } - return services != null ? Collections.unmodifiableList(services) : Collections.EMPTY_LIST; + if (services != null) { + return Collections.unmodifiableList(services); + } else { + return Collections.emptyList(); + } } /** @@ -377,6 +435,7 @@ public final class AccessibilityManager { */ public boolean addAccessibilityStateChangeListener( AccessibilityStateChangeListener listener) { + // Final CopyOnArrayList - no lock needed. return mAccessibilityStateChangeListeners.add(listener); } @@ -388,6 +447,7 @@ public final class AccessibilityManager { */ public boolean removeAccessibilityStateChangeListener( AccessibilityStateChangeListener listener) { + // Final CopyOnArrayList - no lock needed. return mAccessibilityStateChangeListeners.remove(listener); } @@ -400,6 +460,7 @@ public final class AccessibilityManager { */ public boolean addTouchExplorationStateChangeListener( TouchExplorationStateChangeListener listener) { + // Final CopyOnArrayList - no lock needed. return mTouchExplorationStateChangeListeners.add(listener); } @@ -411,6 +472,7 @@ public final class AccessibilityManager { */ public boolean removeTouchExplorationStateChangeListener( TouchExplorationStateChangeListener listener) { + // Final CopyOnArrayList - no lock needed. return mTouchExplorationStateChangeListeners.remove(listener); } @@ -419,50 +481,24 @@ public final class AccessibilityManager { * * @param stateFlags The state flags. */ - private void setState(int stateFlags) { + private void setStateLocked(int stateFlags) { final boolean enabled = (stateFlags & STATE_FLAG_ACCESSIBILITY_ENABLED) != 0; final boolean touchExplorationEnabled = (stateFlags & STATE_FLAG_TOUCH_EXPLORATION_ENABLED) != 0; - synchronized (mHandler) { - final boolean wasEnabled = mIsEnabled; - final boolean wasTouchExplorationEnabled = mIsTouchExplorationEnabled; - // Ensure listeners get current state from isZzzEnabled() calls. - mIsEnabled = enabled; - mIsTouchExplorationEnabled = touchExplorationEnabled; + final boolean wasEnabled = mIsEnabled; + final boolean wasTouchExplorationEnabled = mIsTouchExplorationEnabled; - if (wasEnabled != enabled) { - notifyAccessibilityStateChangedLh(); - } + // Ensure listeners get current state from isZzzEnabled() calls. + mIsEnabled = enabled; + mIsTouchExplorationEnabled = touchExplorationEnabled; - if (wasTouchExplorationEnabled != touchExplorationEnabled) { - notifyTouchExplorationStateChangedLh(); - } + if (wasEnabled != enabled) { + mHandler.sendEmptyMessage(MyHandler.MSG_NOTIFY_ACCESSIBILITY_STATE_CHANGED); } - } - /** - * Notifies the registered {@link AccessibilityStateChangeListener}s. - * <p> - * The caller must be locked on {@link #mHandler}. - */ - private void notifyAccessibilityStateChangedLh() { - final int listenerCount = mAccessibilityStateChangeListeners.size(); - for (int i = 0; i < listenerCount; i++) { - mAccessibilityStateChangeListeners.get(i).onAccessibilityStateChanged(mIsEnabled); - } - } - - /** - * Notifies the registered {@link TouchExplorationStateChangeListener}s. - * <p> - * The caller must be locked on {@link #mHandler}. - */ - private void notifyTouchExplorationStateChangedLh() { - final int listenerCount = mTouchExplorationStateChangeListeners.size(); - for (int i = 0; i < listenerCount; i++) { - mTouchExplorationStateChangeListeners.get(i) - .onTouchExplorationStateChanged(mIsTouchExplorationEnabled); + if (wasTouchExplorationEnabled != touchExplorationEnabled) { + mHandler.sendEmptyMessage(MyHandler.MSG_NOTIFY_EXPLORATION_STATE_CHANGED); } } @@ -475,11 +511,17 @@ public final class AccessibilityManager { */ public int addAccessibilityInteractionConnection(IWindow windowToken, IAccessibilityInteractionConnection connection) { - if (mService == null) { - return View.NO_ID; + final IAccessibilityManager service; + final int userId; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return View.NO_ID; + } + userId = mUserId; } try { - return mService.addAccessibilityInteractionConnection(windowToken, connection, mUserId); + return service.addAccessibilityInteractionConnection(windowToken, connection, userId); } catch (RemoteException re) { Log.e(LOG_TAG, "Error while adding an accessibility interaction connection. ", re); } @@ -493,12 +535,90 @@ public final class AccessibilityManager { * @hide */ public void removeAccessibilityInteractionConnection(IWindow windowToken) { - try { - if (mService != null) { - mService.removeAccessibilityInteractionConnection(windowToken); + final IAccessibilityManager service; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return; } + } + try { + service.removeAccessibilityInteractionConnection(windowToken); } catch (RemoteException re) { Log.e(LOG_TAG, "Error while removing an accessibility interaction connection. ", re); } } + + private IAccessibilityManager getServiceLocked() { + if (mService == null) { + tryConnectToServiceLocked(); + } + return mService; + } + + private void tryConnectToServiceLocked() { + IBinder iBinder = ServiceManager.getService(Context.ACCESSIBILITY_SERVICE); + if (iBinder == null) { + return; + } + IAccessibilityManager service = IAccessibilityManager.Stub.asInterface(iBinder); + try { + final int stateFlags = service.addClient(mClient, mUserId); + setStateLocked(stateFlags); + mService = service; + } catch (RemoteException re) { + Log.e(LOG_TAG, "AccessibilityManagerService is dead", re); + } + } + + /** + * Notifies the registered {@link AccessibilityStateChangeListener}s. + */ + private void handleNotifyAccessibilityStateChanged() { + final boolean isEnabled; + synchronized (mLock) { + isEnabled = mIsEnabled; + } + final int listenerCount = mAccessibilityStateChangeListeners.size(); + for (int i = 0; i < listenerCount; i++) { + mAccessibilityStateChangeListeners.get(i).onAccessibilityStateChanged(isEnabled); + } + } + + /** + * Notifies the registered {@link TouchExplorationStateChangeListener}s. + */ + private void handleNotifyTouchExplorationStateChanged() { + final boolean isTouchExplorationEnabled; + synchronized (mLock) { + isTouchExplorationEnabled = mIsTouchExplorationEnabled; + } + final int listenerCount = mTouchExplorationStateChangeListeners.size(); + for (int i = 0; i < listenerCount; i++) { + mTouchExplorationStateChangeListeners.get(i) + .onTouchExplorationStateChanged(isTouchExplorationEnabled); + } + } + + private final class MyHandler extends Handler { + public static final int MSG_NOTIFY_ACCESSIBILITY_STATE_CHANGED = 1; + public static final int MSG_NOTIFY_EXPLORATION_STATE_CHANGED = 2; + + public MyHandler(Looper looper) { + super(looper, null, false); + } + + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_NOTIFY_ACCESSIBILITY_STATE_CHANGED: { + handleNotifyAccessibilityStateChanged(); + } break; + + case MSG_NOTIFY_EXPLORATION_STATE_CHANGED: { + handleNotifyTouchExplorationStateChanged(); + } + } + } + } } diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java index 4f53c1e..9d10930 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java +++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java @@ -22,8 +22,8 @@ import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.text.InputType; +import android.util.LongArray; import android.util.Pools.SynchronizedPool; -import android.util.SparseLongArray; import android.view.View; import java.util.Collections; @@ -62,13 +62,22 @@ public class AccessibilityNodeInfo implements Parcelable { private static final boolean DEBUG = false; /** @hide */ - public static final int UNDEFINED = -1; + public static final int UNDEFINED_CONNECTION_ID = -1; /** @hide */ - public static final long ROOT_NODE_ID = makeNodeId(UNDEFINED, UNDEFINED); + public static final int UNDEFINED_SELECTION_INDEX = -1; /** @hide */ - public static final int ACTIVE_WINDOW_ID = UNDEFINED; + public static final int UNDEFINED_ITEM_ID = Integer.MAX_VALUE; + + /** @hide */ + public static final long ROOT_NODE_ID = makeNodeId(UNDEFINED_ITEM_ID, UNDEFINED_ITEM_ID); + + /** @hide */ + public static final int ACTIVE_WINDOW_ID = UNDEFINED_ITEM_ID; + + /** @hide */ + public static final int ANY_WINDOW_ID = -2; /** @hide */ public static final int FLAG_PREFETCH_PREDECESSORS = 0x00000001; @@ -282,6 +291,22 @@ public class AccessibilityNodeInfo implements Parcelable { */ public static final int ACTION_DISMISS = 0x00100000; + /** + * Action that sets the text of the node. Performing the action without argument, using <code> + * null</code> or empty {@link CharSequence} will clear the text. This action will also put the + * cursor at the end of text. + * <p> + * <strong>Arguments:</strong> {@link #ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE}<br> + * <strong>Example:</strong> + * <code><pre><p> + * Bundle arguments = new Bundle(); + * arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, + * "android"); + * info.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments); + * </code></pre></p> + */ + public static final int ACTION_SET_TEXT = 0x00200000; + // Action arguments /** @@ -351,6 +376,18 @@ public class AccessibilityNodeInfo implements Parcelable { public static final String ACTION_ARGUMENT_SELECTION_END_INT = "ACTION_ARGUMENT_SELECTION_END_INT"; + /** + * Argument for specifying the text content to set + * <p> + * <strong>Type:</strong> CharSequence<br> + * <strong>Actions:</strong> {@link #ACTION_SET_TEXT} + * </p> + * + * @see #ACTION_SET_TEXT + */ + public static final String ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE = + "ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE"; + // Focus types /** @@ -476,6 +513,13 @@ public class AccessibilityNodeInfo implements Parcelable { * @hide */ public static long makeNodeId(int accessibilityViewId, int virtualDescendantId) { + // We changed the value for undefined node to positive due to wrong + // global id composition (two 32-bin ints into one 64-bit long) but + // the value used for the host node provider view has id -1 so we + // remap it here. + if (virtualDescendantId == AccessibilityNodeProvider.HOST_VIEW_ID) { + virtualDescendantId = UNDEFINED_ITEM_ID; + } return (((long) virtualDescendantId) << VIRTUAL_DESCENDANT_ID_SHIFT) | accessibilityViewId; } @@ -487,7 +531,7 @@ public class AccessibilityNodeInfo implements Parcelable { private boolean mSealed; // Data. - private int mWindowId = UNDEFINED; + private int mWindowId = UNDEFINED_ITEM_ID; private long mSourceNodeId = ROOT_NODE_ID; private long mParentNodeId = ROOT_NODE_ID; private long mLabelForId = ROOT_NODE_ID; @@ -503,19 +547,19 @@ public class AccessibilityNodeInfo implements Parcelable { private CharSequence mContentDescription; private String mViewIdResourceName; - private final SparseLongArray mChildNodeIds = new SparseLongArray(); + private LongArray mChildNodeIds; private int mActions; private int mMovementGranularities; - private int mTextSelectionStart = UNDEFINED; - private int mTextSelectionEnd = UNDEFINED; + private int mTextSelectionStart = UNDEFINED_SELECTION_INDEX; + private int mTextSelectionEnd = UNDEFINED_SELECTION_INDEX; private int mInputType = InputType.TYPE_NULL; private int mLiveRegion = View.ACCESSIBILITY_LIVE_REGION_NONE; private Bundle mExtras; - private int mConnectionId = UNDEFINED; + private int mConnectionId = UNDEFINED_CONNECTION_ID; private RangeInfo mRangeInfo; private CollectionInfo mCollectionInfo; @@ -539,7 +583,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @param source The info source. */ public void setSource(View source) { - setSource(source, UNDEFINED); + setSource(source, UNDEFINED_ITEM_ID); } /** @@ -563,9 +607,9 @@ public class AccessibilityNodeInfo implements Parcelable { */ public void setSource(View root, int virtualDescendantId) { enforceNotSealed(); - mWindowId = (root != null) ? root.getAccessibilityWindowId() : UNDEFINED; + mWindowId = (root != null) ? root.getAccessibilityWindowId() : UNDEFINED_ITEM_ID; final int rootAccessibilityViewId = - (root != null) ? root.getAccessibilityViewId() : UNDEFINED; + (root != null) ? root.getAccessibilityViewId() : UNDEFINED_ITEM_ID; mSourceNodeId = makeNodeId(rootAccessibilityViewId, virtualDescendantId); } @@ -666,21 +710,35 @@ public class AccessibilityNodeInfo implements Parcelable { } /** - * @return The ids of the children. + * Returns the array containing the IDs of this node's children. * * @hide */ - public SparseLongArray getChildNodeIds() { + public LongArray getChildNodeIds() { return mChildNodeIds; } /** + * Returns the id of the child at the specified index. + * + * @throws IndexOutOfBoundsException when index < 0 || index >= + * getChildCount() + * @hide + */ + public long getChildId(int index) { + if (mChildNodeIds == null) { + throw new IndexOutOfBoundsException(); + } + return mChildNodeIds.get(index); + } + + /** * Gets the number of children. * * @return The child count. */ public int getChildCount() { - return mChildNodeIds.size(); + return mChildNodeIds == null ? 0 : mChildNodeIds.size(); } /** @@ -699,6 +757,9 @@ public class AccessibilityNodeInfo implements Parcelable { */ public AccessibilityNodeInfo getChild(int index) { enforceSealed(); + if (mChildNodeIds == null) { + return null; + } if (!canPerformRequestOverConnection(mSourceNodeId)) { return null; } @@ -721,7 +782,35 @@ public class AccessibilityNodeInfo implements Parcelable { * @throws IllegalStateException If called from an AccessibilityService. */ public void addChild(View child) { - addChild(child, UNDEFINED); + addChildInternal(child, UNDEFINED_ITEM_ID, true); + } + + /** + * Unchecked version of {@link #addChild(View)} that does not verify + * uniqueness. For framework use only. + * + * @hide + */ + public void addChildUnchecked(View child) { + addChildInternal(child, UNDEFINED_ITEM_ID, false); + } + + /** + * Removes a child. If the child was not previously added to the node, + * calling this method has no effect. + * <p> + * <strong>Note:</strong> Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * + * @param child The child. + * @return true if the child was present + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public boolean removeChild(View child) { + return removeChild(child, UNDEFINED_ITEM_ID); } /** @@ -739,12 +828,49 @@ public class AccessibilityNodeInfo implements Parcelable { * @param virtualDescendantId The id of the virtual child. */ public void addChild(View root, int virtualDescendantId) { + addChildInternal(root, virtualDescendantId, true); + } + + private void addChildInternal(View root, int virtualDescendantId, boolean checked) { + enforceNotSealed(); + if (mChildNodeIds == null) { + mChildNodeIds = new LongArray(); + } + final int rootAccessibilityViewId = + (root != null) ? root.getAccessibilityViewId() : UNDEFINED_ITEM_ID; + final long childNodeId = makeNodeId(rootAccessibilityViewId, virtualDescendantId); + // If we're checking uniqueness and the ID already exists, abort. + if (checked && mChildNodeIds.indexOf(childNodeId) >= 0) { + return; + } + mChildNodeIds.add(childNodeId); + } + + /** + * Removes a virtual child which is a descendant of the given + * <code>root</code>. If the child was not previously added to the node, + * calling this method has no effect. + * + * @param root The root of the virtual subtree. + * @param virtualDescendantId The id of the virtual child. + * @return true if the child was present + * @see #addChild(View, int) + */ + public boolean removeChild(View root, int virtualDescendantId) { enforceNotSealed(); - final int index = mChildNodeIds.size(); + final LongArray childIds = mChildNodeIds; + if (childIds == null) { + return false; + } final int rootAccessibilityViewId = - (root != null) ? root.getAccessibilityViewId() : UNDEFINED; + (root != null) ? root.getAccessibilityViewId() : UNDEFINED_ITEM_ID; final long childNodeId = makeNodeId(rootAccessibilityViewId, virtualDescendantId); - mChildNodeIds.put(index, childNodeId); + final int index = childIds.indexOf(childNodeId); + if (index < 0) { + return false; + } + childIds.remove(index); + return true; } /** @@ -789,6 +915,24 @@ public class AccessibilityNodeInfo implements Parcelable { } /** + * Removes an action that can be performed on the node. If the action was + * not already added to the node, calling this method has no effect. + * <p> + * <strong>Note:</strong> Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * + * @param action The action. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void removeAction(int action) { + enforceNotSealed(); + mActions &= ~action; + } + + /** * Sets the movement granularities for traversing the text of this node. * <p> * <strong>Note:</strong> Cannot be called from an @@ -915,6 +1059,22 @@ public class AccessibilityNodeInfo implements Parcelable { } /** + * Gets the window to which this node belongs. + * + * @return The window. + * + * @see android.accessibilityservice.AccessibilityService#getWindows() + */ + public AccessibilityWindowInfo getWindow() { + enforceSealed(); + if (!canPerformRequestOverConnection(mSourceNodeId)) { + return null; + } + AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); + return client.getWindow(mConnectionId, mWindowId); + } + + /** * Gets the parent. * <p> * <strong>Note:</strong> It is a client responsibility to recycle the @@ -931,7 +1091,8 @@ public class AccessibilityNodeInfo implements Parcelable { } AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId, - mWindowId, mParentNodeId, false, FLAG_PREFETCH_DESCENDANTS | FLAG_PREFETCH_SIBLINGS); + mWindowId, mParentNodeId, false, FLAG_PREFETCH_PREDECESSORS + | FLAG_PREFETCH_DESCENDANTS | FLAG_PREFETCH_SIBLINGS); } /** @@ -956,7 +1117,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @throws IllegalStateException If called from an AccessibilityService. */ public void setParent(View parent) { - setParent(parent, UNDEFINED); + setParent(parent, UNDEFINED_ITEM_ID); } /** @@ -981,7 +1142,7 @@ public class AccessibilityNodeInfo implements Parcelable { public void setParent(View root, int virtualDescendantId) { enforceNotSealed(); final int rootAccessibilityViewId = - (root != null) ? root.getAccessibilityViewId() : UNDEFINED; + (root != null) ? root.getAccessibilityViewId() : UNDEFINED_ITEM_ID; mParentNodeId = makeNodeId(rootAccessibilityViewId, virtualDescendantId); } @@ -1408,8 +1569,6 @@ public class AccessibilityNodeInfo implements Parcelable { * {@link android.accessibilityservice.AccessibilityService}. * This class is made immutable before being delivered to an AccessibilityService. * </p> - * - * @return collectionItem True if the node is an item. */ public void setCollectionItemInfo(CollectionItemInfo collectionItemInfo) { enforceNotSealed(); @@ -1685,7 +1844,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @param labeled The view for which this info serves as a label. */ public void setLabelFor(View labeled) { - setLabelFor(labeled, UNDEFINED); + setLabelFor(labeled, UNDEFINED_ITEM_ID); } /** @@ -1710,7 +1869,7 @@ public class AccessibilityNodeInfo implements Parcelable { public void setLabelFor(View root, int virtualDescendantId) { enforceNotSealed(); final int rootAccessibilityViewId = (root != null) - ? root.getAccessibilityViewId() : UNDEFINED; + ? root.getAccessibilityViewId() : UNDEFINED_ITEM_ID; mLabelForId = makeNodeId(rootAccessibilityViewId, virtualDescendantId); } @@ -1732,7 +1891,8 @@ public class AccessibilityNodeInfo implements Parcelable { } AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId, - mWindowId, mLabelForId, false, FLAG_PREFETCH_DESCENDANTS | FLAG_PREFETCH_SIBLINGS); + mWindowId, mLabelForId, false, FLAG_PREFETCH_PREDECESSORS + | FLAG_PREFETCH_DESCENDANTS | FLAG_PREFETCH_SIBLINGS); } /** @@ -1742,7 +1902,7 @@ public class AccessibilityNodeInfo implements Parcelable { * @param label The view that labels this node's source. */ public void setLabeledBy(View label) { - setLabeledBy(label, UNDEFINED); + setLabeledBy(label, UNDEFINED_ITEM_ID); } /** @@ -1767,7 +1927,7 @@ public class AccessibilityNodeInfo implements Parcelable { public void setLabeledBy(View root, int virtualDescendantId) { enforceNotSealed(); final int rootAccessibilityViewId = (root != null) - ? root.getAccessibilityViewId() : UNDEFINED; + ? root.getAccessibilityViewId() : UNDEFINED_ITEM_ID; mLabeledById = makeNodeId(rootAccessibilityViewId, virtualDescendantId); } @@ -1789,7 +1949,8 @@ public class AccessibilityNodeInfo implements Parcelable { } AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId, - mWindowId, mLabeledById, false, FLAG_PREFETCH_DESCENDANTS | FLAG_PREFETCH_SIBLINGS); + mWindowId, mLabeledById, false, FLAG_PREFETCH_PREDECESSORS + | FLAG_PREFETCH_DESCENDANTS | FLAG_PREFETCH_SIBLINGS); } /** @@ -1951,6 +2112,7 @@ public class AccessibilityNodeInfo implements Parcelable { /** * {@inheritDoc} */ + @Override public int describeContents() { return 0; } @@ -2114,6 +2276,7 @@ public class AccessibilityNodeInfo implements Parcelable { * is recycled. You must not touch the object after calling this function. * </p> */ + @Override public void writeToParcel(Parcel parcel, int flags) { parcel.writeInt(isSealed() ? 1 : 0); parcel.writeLong(mSourceNodeId); @@ -2123,11 +2286,15 @@ public class AccessibilityNodeInfo implements Parcelable { parcel.writeLong(mLabeledById); parcel.writeInt(mConnectionId); - SparseLongArray childIds = mChildNodeIds; - final int childIdsSize = childIds.size(); - parcel.writeInt(childIdsSize); - for (int i = 0; i < childIdsSize; i++) { - parcel.writeLong(childIds.valueAt(i)); + final LongArray childIds = mChildNodeIds; + if (childIds == null) { + parcel.writeInt(0); + } else { + final int childIdsSize = childIds.size(); + parcel.writeInt(childIdsSize); + for (int i = 0; i < childIdsSize; i++) { + parcel.writeLong(childIds.get(i)); + } } parcel.writeInt(mBoundsInParent.top); @@ -2179,6 +2346,7 @@ public class AccessibilityNodeInfo implements Parcelable { parcel.writeInt(mCollectionInfo.getRowCount()); parcel.writeInt(mCollectionInfo.getColumnCount()); parcel.writeInt(mCollectionInfo.isHierarchical() ? 1 : 0); + parcel.writeInt(mCollectionInfo.getSelectionMode()); } else { parcel.writeInt(0); } @@ -2190,6 +2358,7 @@ public class AccessibilityNodeInfo implements Parcelable { parcel.writeInt(mCollectionItemInfo.getRowIndex()); parcel.writeInt(mCollectionItemInfo.getRowSpan()); parcel.writeInt(mCollectionItemInfo.isHeading() ? 1 : 0); + parcel.writeInt(mCollectionItemInfo.isSelected() ? 1 : 0); } else { parcel.writeInt(0); } @@ -2222,10 +2391,17 @@ public class AccessibilityNodeInfo implements Parcelable { mActions= other.mActions; mBooleanProperties = other.mBooleanProperties; mMovementGranularities = other.mMovementGranularities; - final int otherChildIdCount = other.mChildNodeIds.size(); - for (int i = 0; i < otherChildIdCount; i++) { - mChildNodeIds.put(i, other.mChildNodeIds.valueAt(i)); + + final LongArray otherChildNodeIds = other.mChildNodeIds; + if (otherChildNodeIds != null && otherChildNodeIds.size() > 0) { + if (mChildNodeIds == null) { + mChildNodeIds = otherChildNodeIds.clone(); + } else { + mChildNodeIds.clear(); + mChildNodeIds.addAll(otherChildNodeIds); + } } + mTextSelectionStart = other.mTextSelectionStart; mTextSelectionEnd = other.mTextSelectionEnd; mInputType = other.mInputType; @@ -2255,11 +2431,15 @@ public class AccessibilityNodeInfo implements Parcelable { mLabeledById = parcel.readLong(); mConnectionId = parcel.readInt(); - SparseLongArray childIds = mChildNodeIds; final int childrenSize = parcel.readInt(); - for (int i = 0; i < childrenSize; i++) { - final long childId = parcel.readLong(); - childIds.put(i, childId); + if (childrenSize <= 0) { + mChildNodeIds = null; + } else { + mChildNodeIds = new LongArray(childrenSize); + for (int i = 0; i < childrenSize; i++) { + final long childId = parcel.readLong(); + mChildNodeIds.add(childId); + } } mBoundsInParent.top = parcel.readInt(); @@ -2306,7 +2486,8 @@ public class AccessibilityNodeInfo implements Parcelable { mCollectionInfo = CollectionInfo.obtain( parcel.readInt(), parcel.readInt(), - parcel.readInt() == 1); + parcel.readInt() == 1, + parcel.readInt()); } if (parcel.readInt() == 1) { @@ -2315,6 +2496,7 @@ public class AccessibilityNodeInfo implements Parcelable { parcel.readInt(), parcel.readInt(), parcel.readInt(), + parcel.readInt() == 1, parcel.readInt() == 1); } } @@ -2328,10 +2510,12 @@ public class AccessibilityNodeInfo implements Parcelable { mParentNodeId = ROOT_NODE_ID; mLabelForId = ROOT_NODE_ID; mLabeledById = ROOT_NODE_ID; - mWindowId = UNDEFINED; - mConnectionId = UNDEFINED; + mWindowId = UNDEFINED_ITEM_ID; + mConnectionId = UNDEFINED_CONNECTION_ID; mMovementGranularities = 0; - mChildNodeIds.clear(); + if (mChildNodeIds != null) { + mChildNodeIds.clear(); + } mBoundsInParent.set(0, 0, 0, 0); mBoundsInScreen.set(0, 0, 0, 0); mBooleanProperties = 0; @@ -2341,8 +2525,8 @@ public class AccessibilityNodeInfo implements Parcelable { mContentDescription = null; mViewIdResourceName = null; mActions = 0; - mTextSelectionStart = UNDEFINED; - mTextSelectionEnd = UNDEFINED; + mTextSelectionStart = UNDEFINED_SELECTION_INDEX; + mTextSelectionEnd = UNDEFINED_SELECTION_INDEX; mInputType = InputType.TYPE_NULL; mLiveRegion = View.ACCESSIBILITY_LIVE_REGION_NONE; if (mExtras != null) { @@ -2435,9 +2619,9 @@ public class AccessibilityNodeInfo implements Parcelable { } private boolean canPerformRequestOverConnection(long accessibilityNodeId) { - return (mWindowId != UNDEFINED - && getAccessibilityViewId(accessibilityNodeId) != UNDEFINED - && mConnectionId != UNDEFINED); + return (mWindowId != UNDEFINED_ITEM_ID + && getAccessibilityViewId(accessibilityNodeId) != UNDEFINED_ITEM_ID + && mConnectionId != UNDEFINED_CONNECTION_ID); } @Override @@ -2477,6 +2661,7 @@ public class AccessibilityNodeInfo implements Parcelable { builder.append(super.toString()); if (DEBUG) { + builder.append("; sourceNodeId: " + mSourceNodeId); builder.append("; accessibilityViewId: " + getAccessibilityViewId(mSourceNodeId)); builder.append("; virtualDescendantId: " + getVirtualDescendantId(mSourceNodeId)); builder.append("; mParentNodeId: " + mParentNodeId); @@ -2493,12 +2678,14 @@ public class AccessibilityNodeInfo implements Parcelable { } builder.append("]"); - SparseLongArray childIds = mChildNodeIds; builder.append("; childAccessibilityIds: ["); - for (int i = 0, count = childIds.size(); i < count; i++) { - builder.append(childIds.valueAt(i)); - if (i < count - 1) { - builder.append(", "); + final LongArray childIds = mChildNodeIds; + if (childIds != null) { + for (int i = 0, count = childIds.size(); i < count; i++) { + builder.append(childIds.get(i)); + if (i < count - 1) { + builder.append(", "); + } } } builder.append("]"); @@ -2668,6 +2855,15 @@ public class AccessibilityNodeInfo implements Parcelable { * </p> */ public static final class CollectionInfo { + /** Selection mode where items are not selectable. */ + public static final int SELECTION_MODE_NONE = 0; + + /** Selection mode where a single item may be selected. */ + public static final int SELECTION_MODE_SINGLE = 1; + + /** Selection mode where multiple items may be selected. */ + public static final int SELECTION_MODE_MULTIPLE = 2; + private static final int MAX_POOL_SIZE = 20; private static final SynchronizedPool<CollectionInfo> sPool = @@ -2676,17 +2872,17 @@ public class AccessibilityNodeInfo implements Parcelable { private int mRowCount; private int mColumnCount; private boolean mHierarchical; + private int mSelectionMode; /** * Obtains a pooled instance that is a clone of another one. * * @param other The instance to clone. - * * @hide */ public static CollectionInfo obtain(CollectionInfo other) { - return CollectionInfo.obtain(other.mRowCount, other.mColumnCount, - other.mHierarchical); + return CollectionInfo.obtain(other.mRowCount, other.mColumnCount, other.mHierarchical, + other.mSelectionMode); } /** @@ -2698,9 +2894,34 @@ public class AccessibilityNodeInfo implements Parcelable { */ public static CollectionInfo obtain(int rowCount, int columnCount, boolean hierarchical) { - CollectionInfo info = sPool.acquire(); - return (info != null) ? info : new CollectionInfo(rowCount, - columnCount, hierarchical); + return obtain(rowCount, columnCount, hierarchical, SELECTION_MODE_NONE); + } + + /** + * Obtains a pooled instance. + * + * @param rowCount The number of rows. + * @param columnCount The number of columns. + * @param hierarchical Whether the collection is hierarchical. + * @param selectionMode The collection's selection mode, one of: + * <ul> + * <li>{@link #SELECTION_MODE_NONE} + * <li>{@link #SELECTION_MODE_SINGLE} + * <li>{@link #SELECTION_MODE_MULTIPLE} + * </ul> + */ + public static CollectionInfo obtain(int rowCount, int columnCount, + boolean hierarchical, int selectionMode) { + final CollectionInfo info = sPool.acquire(); + if (info == null) { + return new CollectionInfo(rowCount, columnCount, hierarchical, selectionMode); + } + + info.mRowCount = rowCount; + info.mColumnCount = columnCount; + info.mHierarchical = hierarchical; + info.mSelectionMode = selectionMode; + return info; } /** @@ -2709,12 +2930,14 @@ public class AccessibilityNodeInfo implements Parcelable { * @param rowCount The number of rows. * @param columnCount The number of columns. * @param hierarchical Whether the collection is hierarchical. + * @param selectionMode The collection's selection mode. */ - private CollectionInfo(int rowCount, int columnCount, - boolean hierarchical) { + private CollectionInfo(int rowCount, int columnCount, boolean hierarchical, + int selectionMode) { mRowCount = rowCount; mColumnCount = columnCount; mHierarchical = hierarchical; + mSelectionMode = selectionMode; } /** @@ -2745,6 +2968,20 @@ public class AccessibilityNodeInfo implements Parcelable { } /** + * Gets the collection's selection mode. + * + * @return The collection's selection mode, one of: + * <ul> + * <li>{@link #SELECTION_MODE_NONE} + * <li>{@link #SELECTION_MODE_SINGLE} + * <li>{@link #SELECTION_MODE_MULTIPLE} + * </ul> + */ + public int getSelectionMode() { + return mSelectionMode; + } + + /** * Recycles this instance. */ void recycle() { @@ -2756,6 +2993,7 @@ public class AccessibilityNodeInfo implements Parcelable { mRowCount = 0; mColumnCount = 0; mHierarchical = false; + mSelectionMode = SELECTION_MODE_NONE; } } @@ -2781,12 +3019,11 @@ public class AccessibilityNodeInfo implements Parcelable { * Obtains a pooled instance that is a clone of another one. * * @param other The instance to clone. - * * @hide */ public static CollectionItemInfo obtain(CollectionItemInfo other) { - return CollectionItemInfo.obtain(other.mRowIndex, other.mRowSpan, - other.mColumnIndex, other.mColumnSpan, other.mHeading); + return CollectionItemInfo.obtain(other.mRowIndex, other.mRowSpan, other.mColumnIndex, + other.mColumnSpan, other.mHeading, other.mSelected); } /** @@ -2800,9 +3037,34 @@ public class AccessibilityNodeInfo implements Parcelable { */ public static CollectionItemInfo obtain(int rowIndex, int rowSpan, int columnIndex, int columnSpan, boolean heading) { - CollectionItemInfo info = sPool.acquire(); - return (info != null) ? info : new CollectionItemInfo(rowIndex, - rowSpan, columnIndex, columnSpan, heading); + return obtain(rowIndex, rowSpan, columnIndex, columnSpan, heading, false); + } + + /** + * Obtains a pooled instance. + * + * @param rowIndex The row index at which the item is located. + * @param rowSpan The number of rows the item spans. + * @param columnIndex The column index at which the item is located. + * @param columnSpan The number of columns the item spans. + * @param heading Whether the item is a heading. + * @param selected Whether the item is selected. + */ + public static CollectionItemInfo obtain(int rowIndex, int rowSpan, + int columnIndex, int columnSpan, boolean heading, boolean selected) { + final CollectionItemInfo info = sPool.acquire(); + if (info == null) { + return new CollectionItemInfo( + rowIndex, rowSpan, columnIndex, columnSpan, heading, selected); + } + + info.mRowIndex = rowIndex; + info.mRowSpan = rowSpan; + info.mColumnIndex = columnIndex; + info.mColumnSpan = columnSpan; + info.mHeading = heading; + info.mSelected = selected; + return info; } private boolean mHeading; @@ -2810,6 +3072,7 @@ public class AccessibilityNodeInfo implements Parcelable { private int mRowIndex; private int mColumnSpan; private int mRowSpan; + private boolean mSelected; /** * Creates a new instance. @@ -2820,13 +3083,14 @@ public class AccessibilityNodeInfo implements Parcelable { * @param columnSpan The number of columns the item spans. * @param heading Whether the item is a heading. */ - private CollectionItemInfo(int rowIndex, int rowSpan, - int columnIndex, int columnSpan, boolean heading) { + private CollectionItemInfo(int rowIndex, int rowSpan, int columnIndex, int columnSpan, + boolean heading, boolean selected) { mRowIndex = rowIndex; mRowSpan = rowSpan; mColumnIndex = columnIndex; mColumnSpan = columnSpan; mHeading = heading; + mSelected = selected; } /** @@ -2876,6 +3140,15 @@ public class AccessibilityNodeInfo implements Parcelable { } /** + * Gets if the collection item is selected. + * + * @return If the item is selected. + */ + public boolean isSelected() { + return mSelected; + } + + /** * Recycles this instance. */ void recycle() { @@ -2889,20 +3162,23 @@ public class AccessibilityNodeInfo implements Parcelable { mRowIndex = 0; mRowSpan = 0; mHeading = false; + mSelected = false; } } /** - * @see Parcelable.Creator + * @see android.os.Parcelable.Creator */ public static final Parcelable.Creator<AccessibilityNodeInfo> CREATOR = new Parcelable.Creator<AccessibilityNodeInfo>() { + @Override public AccessibilityNodeInfo createFromParcel(Parcel parcel) { AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(); info.initFromParcel(parcel); return info; } + @Override public AccessibilityNodeInfo[] newArray(int size) { return new AccessibilityNodeInfo[size]; } diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfoCache.java b/core/java/android/view/accessibility/AccessibilityNodeInfoCache.java deleted file mode 100644 index a9473a8..0000000 --- a/core/java/android/view/accessibility/AccessibilityNodeInfoCache.java +++ /dev/null @@ -1,329 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * 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.view.accessibility; - -import android.os.Build; -import android.util.Log; -import android.util.LongSparseArray; -import android.util.SparseLongArray; - -import java.util.HashSet; -import java.util.LinkedList; -import java.util.Queue; - -/** - * Simple cache for AccessibilityNodeInfos. The cache is mapping an - * accessibility id to an info. The cache allows storing of - * <code>null</code> values. It also tracks accessibility events - * and invalidates accordingly. - * - * @hide - */ -public class AccessibilityNodeInfoCache { - - private static final String LOG_TAG = AccessibilityNodeInfoCache.class.getSimpleName(); - - private static final boolean ENABLED = true; - - private static final boolean DEBUG = false; - - private static final boolean CHECK_INTEGRITY_IF_DEBUGGABLE_BUILD = true; - - private final Object mLock = new Object(); - - private final LongSparseArray<AccessibilityNodeInfo> mCacheImpl; - - private int mWindowId; - - public AccessibilityNodeInfoCache() { - if (ENABLED) { - mCacheImpl = new LongSparseArray<AccessibilityNodeInfo>(); - } else { - mCacheImpl = null; - } - } - - /** - * The cache keeps track of {@link AccessibilityEvent}s and invalidates - * cached nodes as appropriate. - * - * @param event An event. - */ - public void onAccessibilityEvent(AccessibilityEvent event) { - if (ENABLED) { - final int eventType = event.getEventType(); - switch (eventType) { - case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END: - case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: - case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER: - case AccessibilityEvent.TYPE_VIEW_HOVER_EXIT: { - // If the active window changes, clear the cache. - final int windowId = event.getWindowId(); - if (mWindowId != windowId) { - mWindowId = windowId; - clear(); - } - } break; - case AccessibilityEvent.TYPE_VIEW_FOCUSED: - case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED: - case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED: - case AccessibilityEvent.TYPE_VIEW_SELECTED: - case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED: - case AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED: { - refreshCachedNode(event.getSourceNodeId()); - } break; - case AccessibilityEvent.TYPE_VIEW_SCROLLED: { - synchronized (mLock) { - clearSubTreeLocked(event.getSourceNodeId()); - } - } break; - case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: { - synchronized (mLock) { - final long sourceId = event.getSourceNodeId(); - if ((event.getContentChangeTypes() - & AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE) != 0) { - clearSubTreeLocked(sourceId); - } else { - refreshCachedNode(sourceId); - } - } - } break; - } - if (CHECK_INTEGRITY_IF_DEBUGGABLE_BUILD && Build.IS_DEBUGGABLE) { - checkIntegrity(); - } - } - } - - private void refreshCachedNode(long sourceId) { - if (DEBUG) { - Log.i(LOG_TAG, "Refreshing cached node."); - } - synchronized (mLock) { - AccessibilityNodeInfo cachedInfo = mCacheImpl.get(sourceId); - // If the source is not in the cache - nothing to do. - if (cachedInfo == null) { - return; - } - // The node changed so we will just refresh it right now. - if (cachedInfo.refresh(true)) { - return; - } - // Weird, we could not refresh. Just evict the entire sub-tree. - clearSubTreeLocked(sourceId); - } - } - - /** - * Gets a cached {@link AccessibilityNodeInfo} given its accessibility node id. - * - * @param accessibilityNodeId The info accessibility node id. - * @return The cached {@link AccessibilityNodeInfo} or null if such not found. - */ - public AccessibilityNodeInfo get(long accessibilityNodeId) { - if (ENABLED) { - synchronized(mLock) { - AccessibilityNodeInfo info = mCacheImpl.get(accessibilityNodeId); - if (info != null) { - // Return a copy since the client calls to AccessibilityNodeInfo#recycle() - // will wipe the data of the cached info. - info = AccessibilityNodeInfo.obtain(info); - } - if (DEBUG) { - Log.i(LOG_TAG, "get(" + accessibilityNodeId + ") = " + info); - } - return info; - } - } else { - return null; - } - } - - /** - * Caches an {@link AccessibilityNodeInfo} given its accessibility node id. - * - * @param info The {@link AccessibilityNodeInfo} to cache. - */ - public void add(AccessibilityNodeInfo info) { - if (ENABLED) { - synchronized(mLock) { - if (DEBUG) { - Log.i(LOG_TAG, "add(" + info + ")"); - } - - final long sourceId = info.getSourceNodeId(); - AccessibilityNodeInfo oldInfo = mCacheImpl.get(sourceId); - if (oldInfo != null) { - // If the added node is in the cache we have to be careful if - // the new one represents a source state where some of the - // children have been removed to avoid having disconnected - // subtrees in the cache. - SparseLongArray oldChildrenIds = oldInfo.getChildNodeIds(); - SparseLongArray newChildrenIds = info.getChildNodeIds(); - final int oldChildCount = oldChildrenIds.size(); - for (int i = 0; i < oldChildCount; i++) { - final long oldChildId = oldChildrenIds.valueAt(i); - if (newChildrenIds.indexOfValue(oldChildId) < 0) { - clearSubTreeLocked(oldChildId); - } - } - - // Also be careful if the parent has changed since the new - // parent may be a predecessor of the old parent which will - // make the cached tree cyclic. - final long oldParentId = oldInfo.getParentNodeId(); - if (info.getParentNodeId() != oldParentId) { - clearSubTreeLocked(oldParentId); - } - } - - // Cache a copy since the client calls to AccessibilityNodeInfo#recycle() - // will wipe the data of the cached info. - AccessibilityNodeInfo clone = AccessibilityNodeInfo.obtain(info); - mCacheImpl.put(sourceId, clone); - } - } - } - - /** - * Clears the cache. - */ - public void clear() { - if (ENABLED) { - synchronized(mLock) { - if (DEBUG) { - Log.i(LOG_TAG, "clear()"); - } - // Recycle the nodes before clearing the cache. - final int nodeCount = mCacheImpl.size(); - for (int i = 0; i < nodeCount; i++) { - AccessibilityNodeInfo info = mCacheImpl.valueAt(i); - info.recycle(); - } - mCacheImpl.clear(); - } - } - } - - /** - * Clears a subtree rooted at the node with the given id. - * - * @param rootNodeId The root id. - */ - private void clearSubTreeLocked(long rootNodeId) { - if (DEBUG) { - Log.i(LOG_TAG, "Clearing cached subtree."); - } - clearSubTreeRecursiveLocked(rootNodeId); - } - - private void clearSubTreeRecursiveLocked(long rootNodeId) { - AccessibilityNodeInfo current = mCacheImpl.get(rootNodeId); - if (current == null) { - return; - } - mCacheImpl.remove(rootNodeId); - SparseLongArray childNodeIds = current.getChildNodeIds(); - final int childCount = childNodeIds.size(); - for (int i = 0; i < childCount; i++) { - final long childNodeId = childNodeIds.valueAt(i); - clearSubTreeRecursiveLocked(childNodeId); - } - } - - /** - * Check the integrity of the cache which is it does not have nodes - * from more than one window, there are no duplicates, all nodes are - * connected, there is a single input focused node, and there is a - * single accessibility focused node. - */ - private void checkIntegrity() { - synchronized (mLock) { - // Get the root. - if (mCacheImpl.size() <= 0) { - return; - } - - // If the cache is a tree it does not matter from - // which node we start to search for the root. - AccessibilityNodeInfo root = mCacheImpl.valueAt(0); - AccessibilityNodeInfo parent = root; - while (parent != null) { - root = parent; - parent = mCacheImpl.get(parent.getParentNodeId()); - } - - // Traverse the tree and do some checks. - final int windowId = root.getWindowId(); - AccessibilityNodeInfo accessFocus = null; - AccessibilityNodeInfo inputFocus = null; - HashSet<AccessibilityNodeInfo> seen = new HashSet<AccessibilityNodeInfo>(); - Queue<AccessibilityNodeInfo> fringe = new LinkedList<AccessibilityNodeInfo>(); - fringe.add(root); - - while (!fringe.isEmpty()) { - AccessibilityNodeInfo current = fringe.poll(); - // Check for duplicates - if (!seen.add(current)) { - Log.e(LOG_TAG, "Duplicate node: " + current); - return; - } - - // Check for one accessibility focus. - if (current.isAccessibilityFocused()) { - if (accessFocus != null) { - Log.e(LOG_TAG, "Duplicate accessibility focus:" + current); - } else { - accessFocus = current; - } - } - - // Check for one input focus. - if (current.isFocused()) { - if (inputFocus != null) { - Log.e(LOG_TAG, "Duplicate input focus: " + current); - } else { - inputFocus = current; - } - } - - SparseLongArray childIds = current.getChildNodeIds(); - final int childCount = childIds.size(); - for (int i = 0; i < childCount; i++) { - final long childId = childIds.valueAt(i); - AccessibilityNodeInfo child = mCacheImpl.get(childId); - if (child != null) { - fringe.add(child); - } - } - } - - // Check for disconnected nodes or ones from another window. - for (int i = 0; i < mCacheImpl.size(); i++) { - AccessibilityNodeInfo info = mCacheImpl.valueAt(i); - if (!seen.contains(info)) { - if (info.getWindowId() == windowId) { - Log.e(LOG_TAG, "Disconneced node: " + info); - } else { - Log.e(LOG_TAG, "Node from: " + info.getWindowId() + " not from:" - + windowId + " " + info); - } - } - } - } - } -} diff --git a/core/java/android/view/accessibility/AccessibilityNodeProvider.java b/core/java/android/view/accessibility/AccessibilityNodeProvider.java index 718c32f..abcbb70 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeProvider.java +++ b/core/java/android/view/accessibility/AccessibilityNodeProvider.java @@ -70,9 +70,14 @@ import java.util.List; public abstract class AccessibilityNodeProvider { /** + * The virtual id for the hosting View. + */ + public static final int HOST_VIEW_ID = -1; + + /** * Returns an {@link AccessibilityNodeInfo} representing a virtual view, * i.e. a descendant of the host View, with the given <code>virtualViewId</code> - * or the host View itself if <code>virtualViewId</code> equals to {@link View#NO_ID}. + * or the host View itself if <code>virtualViewId</code> equals to {@link #HOST_VIEW_ID}. * <p> * A virtual descendant is an imaginary View that is reported as a part of the view * hierarchy for accessibility purposes. This enables custom views that draw complex @@ -99,7 +104,7 @@ public abstract class AccessibilityNodeProvider { /** * Performs an accessibility action on a virtual view, i.e. a descendant of the * host View, with the given <code>virtualViewId</code> or the host View itself - * if <code>virtualViewId</code> equals to {@link View#NO_ID}. + * if <code>virtualViewId</code> equals to {@link #HOST_VIEW_ID}. * * @param virtualViewId A client defined virtual view id. * @param action The action to perform. @@ -117,8 +122,8 @@ public abstract class AccessibilityNodeProvider { /** * Finds {@link AccessibilityNodeInfo}s by text. The match is case insensitive * containment. The search is relative to the virtual view, i.e. a descendant of the - * host View, with the given <code>virtualViewId</code> or the host View itself - * <code>virtualViewId</code> equals to {@link View#NO_ID}. + * host View, with the given <code>virtualViewId</code> or the host View itself + * <code>virtualViewId</code> equals to {@link #HOST_VIEW_ID}. * * @param virtualViewId A client defined virtual view id which defined * the root of the tree in which to perform the search. diff --git a/core/java/android/view/accessibility/AccessibilityRecord.aidl b/core/java/android/view/accessibility/AccessibilityRecord.aidl new file mode 100644 index 0000000..d542af0 --- /dev/null +++ b/core/java/android/view/accessibility/AccessibilityRecord.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2014, 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.view.accessibility; + +parcelable AccessibilityRecord; diff --git a/core/java/android/view/accessibility/AccessibilityRecord.java b/core/java/android/view/accessibility/AccessibilityRecord.java index 3fcd218..cc6a71d 100644 --- a/core/java/android/view/accessibility/AccessibilityRecord.java +++ b/core/java/android/view/accessibility/AccessibilityRecord.java @@ -78,7 +78,7 @@ public class AccessibilityRecord { private boolean mIsInPool; boolean mSealed; - int mBooleanProperties = PROPERTY_IMPORTANT_FOR_ACCESSIBILITY; + int mBooleanProperties = 0; int mCurrentItemIndex = UNDEFINED; int mItemCount = UNDEFINED; int mFromIndex = UNDEFINED; @@ -791,7 +791,7 @@ public class AccessibilityRecord { */ void clear() { mSealed = false; - mBooleanProperties = PROPERTY_IMPORTANT_FOR_ACCESSIBILITY; + mBooleanProperties = 0; mCurrentItemIndex = UNDEFINED; mItemCount = UNDEFINED; mFromIndex = UNDEFINED; diff --git a/core/java/android/view/accessibility/AccessibilityWindowInfo.aidl b/core/java/android/view/accessibility/AccessibilityWindowInfo.aidl new file mode 100644 index 0000000..fdb25fb --- /dev/null +++ b/core/java/android/view/accessibility/AccessibilityWindowInfo.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2014, 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.view.accessibility; + +parcelable AccessibilityWindowInfo; diff --git a/core/java/android/view/accessibility/AccessibilityWindowInfo.java b/core/java/android/view/accessibility/AccessibilityWindowInfo.java new file mode 100644 index 0000000..80b5c50 --- /dev/null +++ b/core/java/android/view/accessibility/AccessibilityWindowInfo.java @@ -0,0 +1,574 @@ +/* + * Copyright (C) 2014 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.view.accessibility; + +import android.graphics.Rect; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.LongArray; +import android.util.Pools.SynchronizedPool; + +/** + * This class represents a state snapshot of a window for accessibility + * purposes. The screen content contains one or more windows where some + * windows can be descendants of other windows, which is the windows are + * hierarchically ordered. Note that there is no root window. Hence, the + * screen content can be seen as a collection of window trees. + */ +public final class AccessibilityWindowInfo implements Parcelable { + + private static final boolean DEBUG = false; + + /** + * Window type: This is an application window. Such a window shows UI for + * interacting with an application. + */ + public static final int TYPE_APPLICATION = 1; + + /** + * Window type: This is an input method window. Such a window shows UI for + * inputting text such as keyboard, suggestions, etc. + */ + public static final int TYPE_INPUT_METHOD = 2; + + /** + * Window type: This is an system window. Such a window shows UI for + * interacting with the system. + */ + public static final int TYPE_SYSTEM = 3; + + private static final int UNDEFINED = -1; + + private static final int BOOLEAN_PROPERTY_ACTIVE = 1 << 0; + private static final int BOOLEAN_PROPERTY_FOCUSED = 1 << 1; + + // Housekeeping. + private static final int MAX_POOL_SIZE = 10; + private static final SynchronizedPool<AccessibilityWindowInfo> sPool = + new SynchronizedPool<AccessibilityWindowInfo>(MAX_POOL_SIZE); + + // Data. + private int mType = UNDEFINED; + private int mLayer = UNDEFINED; + private int mBooleanProperties; + private int mId = UNDEFINED; + private int mParentId = UNDEFINED; + private final Rect mBoundsInScreen = new Rect(); + private LongArray mChildIds; + + private int mConnectionId = UNDEFINED; + + private AccessibilityWindowInfo() { + /* do nothing - hide constructor */ + } + + /** + * Gets the type of the window. + * + * @return The type. + * + * @see #TYPE_APPLICATION + * @see #TYPE_INPUT_METHOD + * @see #TYPE_SYSTEM + */ + public int getType() { + return mType; + } + + /** + * Sets the type of the window. + * + * @param The type + * + * @hide + */ + public void setType(int type) { + mType = type; + } + + /** + * Gets the layer which determines the Z-order of the window. Windows + * with greater layer appear on top of windows with lesser layer. + * + * @return The window layer. + */ + public int getLayer() { + return mLayer; + } + + /** + * Sets the layer which determines the Z-order of the window. Windows + * with greater layer appear on top of windows with lesser layer. + * + * @param The window layer. + * + * @hide + */ + public void setLayer(int layer) { + mLayer = layer; + } + + /** + * Gets the root node in the window's hierarchy. + * + * @return The root node. + */ + public AccessibilityNodeInfo getRoot() { + if (mConnectionId == UNDEFINED) { + return null; + } + AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); + return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId, + mId, AccessibilityNodeInfo.ROOT_NODE_ID, + true, AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS); + } + + /** + * Gets the parent window if such. + * + * @return The parent window. + */ + public AccessibilityWindowInfo getParent() { + if (mConnectionId == UNDEFINED || mParentId == UNDEFINED) { + return null; + } + AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); + return client.getWindow(mConnectionId, mParentId); + } + + /** + * Sets the parent window id. + * + * @param parentId The parent id. + * + * @hide + */ + public void setParentId(int parentId) { + mParentId = parentId; + } + + /** + * Gets the unique window id. + * + * @return windowId The window id. + */ + public int getId() { + return mId; + } + + /** + * Sets the unique window id. + * + * @param windowId The window id. + * + * @hide + */ + public void setId(int id) { + mId = id; + } + + /** + * Sets the unique id of the IAccessibilityServiceConnection over which + * this instance can send requests to the system. + * + * @param connectionId The connection id. + * + * @hide + */ + public void setConnectionId(int connectionId) { + mConnectionId = connectionId; + } + + /** + * Gets the bounds of this window in the screen. + * + * @param outBounds The out window bounds. + */ + public void getBoundsInScreen(Rect outBounds) { + outBounds.set(mBoundsInScreen); + } + + /** + * Sets the bounds of this window in the screen. + * + * @param bounds The out window bounds. + * + * @hide + */ + public void setBoundsInScreen(Rect bounds) { + mBoundsInScreen.set(bounds); + } + + /** + * Gets if this window is active. An active window is the one + * the user is currently touching or the window has input focus + * and the user is not touching any window. + * + * @return Whether this is the active window. + */ + public boolean isActive() { + return getBooleanProperty(BOOLEAN_PROPERTY_ACTIVE); + } + + /** + * Sets if this window is active, which is this is the window + * the user is currently touching or the window has input focus + * and the user is not touching any window. + * + * @param Whether this is the active window. + * + * @hide + */ + public void setActive(boolean active) { + setBooleanProperty(BOOLEAN_PROPERTY_ACTIVE, active); + } + + /** + * Gets if this window has input focus. + * + * @return Whether has input focus. + */ + public boolean isFocused() { + return getBooleanProperty(BOOLEAN_PROPERTY_FOCUSED); + } + + /** + * Sets if this window has input focus. + * + * @param Whether has input focus. + * + * @hide + */ + public void setFocused(boolean focused) { + setBooleanProperty(BOOLEAN_PROPERTY_FOCUSED, focused); + } + + /** + * Gets the number of child windows. + * + * @return The child count. + */ + public int getChildCount() { + return (mChildIds != null) ? mChildIds.size() : 0; + } + + /** + * Gets the child window at a given index. + * + * @param index The index. + * @return The child. + */ + public AccessibilityWindowInfo getChild(int index) { + if (mChildIds == null) { + throw new IndexOutOfBoundsException(); + } + if (mConnectionId == UNDEFINED) { + return null; + } + final int childId = (int) mChildIds.get(index); + AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); + return client.getWindow(mConnectionId, childId); + } + + /** + * Adds a child window. + * + * @param childId The child window id. + * + * @hide + */ + public void addChild(int childId) { + if (mChildIds == null) { + mChildIds = new LongArray(); + } + mChildIds.add(childId); + } + + /** + * Returns a cached instance if such is available or a new one is + * created. + * + * @return An instance. + */ + public static AccessibilityWindowInfo obtain() { + AccessibilityWindowInfo info = sPool.acquire(); + if (info == null) { + info = new AccessibilityWindowInfo(); + } + return info; + } + + /** + * Returns a cached instance if such is available or a new one is + * created. The returned instance is initialized from the given + * <code>info</code>. + * + * @param info The other info. + * @return An instance. + */ + public static AccessibilityWindowInfo obtain(AccessibilityWindowInfo info) { + AccessibilityWindowInfo infoClone = obtain(); + + infoClone.mType = info.mType; + infoClone.mLayer = info.mLayer; + infoClone.mBooleanProperties = info.mBooleanProperties; + infoClone.mId = info.mId; + infoClone.mParentId = info.mParentId; + infoClone.mBoundsInScreen.set(info.mBoundsInScreen); + + if (info.mChildIds != null && info.mChildIds.size() > 0) { + if (infoClone.mChildIds == null) { + infoClone.mChildIds = info.mChildIds.clone(); + } else { + infoClone.mChildIds.addAll(info.mChildIds); + } + } + + infoClone.mConnectionId = info.mConnectionId; + + return infoClone; + } + + /** + * Return an instance back to be reused. + * <p> + * <strong>Note:</strong> You must not touch the object after calling this function. + * </p> + * + * @throws IllegalStateException If the info is already recycled. + */ + public void recycle() { + clear(); + sPool.release(this); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeInt(mType); + parcel.writeInt(mLayer); + parcel.writeInt(mBooleanProperties); + parcel.writeInt(mId); + parcel.writeInt(mParentId); + mBoundsInScreen.writeToParcel(parcel, flags); + + final LongArray childIds = mChildIds; + if (childIds == null) { + parcel.writeInt(0); + } else { + final int childCount = childIds.size(); + parcel.writeInt(childCount); + for (int i = 0; i < childCount; i++) { + parcel.writeInt((int) childIds.get(i)); + } + } + + parcel.writeInt(mConnectionId); + } + + private void initFromParcel(Parcel parcel) { + mType = parcel.readInt(); + mLayer = parcel.readInt(); + mBooleanProperties = parcel.readInt(); + mId = parcel.readInt(); + mParentId = parcel.readInt(); + mBoundsInScreen.readFromParcel(parcel); + + final int childCount = parcel.readInt(); + if (childCount > 0) { + if (mChildIds == null) { + mChildIds = new LongArray(childCount); + } + for (int i = 0; i < childCount; i++) { + final int childId = parcel.readInt(); + mChildIds.add(childId); + } + } + + mConnectionId = parcel.readInt(); + } + + @Override + public int hashCode() { + return mId; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + AccessibilityWindowInfo other = (AccessibilityWindowInfo) obj; + return (mId == other.mId); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("AccessibilityWindowInfo["); + builder.append("id=").append(mId); + builder.append(", type=").append(typeToString(mType)); + builder.append(", layer=").append(mLayer); + builder.append(", bounds=").append(mBoundsInScreen); + builder.append(", focused=").append(isFocused()); + builder.append(", active=").append(isActive()); + if (DEBUG) { + builder.append(", parent=").append(mParentId); + builder.append(", children=["); + if (mChildIds != null) { + final int childCount = mChildIds.size(); + for (int i = 0; i < childCount; i++) { + builder.append(mChildIds.get(i)); + if (i < childCount - 1) { + builder.append(','); + } + } + } else { + builder.append("null"); + } + builder.append(']'); + } else { + builder.append(", hasParent=").append(mParentId != UNDEFINED); + builder.append(", hasChildren=").append(mChildIds != null + && mChildIds.size() > 0); + } + builder.append(']'); + return builder.toString(); + } + + /** + * Clears the internal state. + */ + private void clear() { + mType = UNDEFINED; + mLayer = UNDEFINED; + mBooleanProperties = 0; + mId = UNDEFINED; + mParentId = UNDEFINED; + mBoundsInScreen.setEmpty(); + if (mChildIds != null) { + mChildIds.clear(); + } + mConnectionId = UNDEFINED; + } + + /** + * Gets the value of a boolean property. + * + * @param property The property. + * @return The value. + */ + private boolean getBooleanProperty(int property) { + return (mBooleanProperties & property) != 0; + } + + /** + * Sets a boolean property. + * + * @param property The property. + * @param value The value. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + private void setBooleanProperty(int property, boolean value) { + if (value) { + mBooleanProperties |= property; + } else { + mBooleanProperties &= ~property; + } + } + + private static String typeToString(int type) { + switch (type) { + case TYPE_APPLICATION: { + return "TYPE_APPLICATION"; + } + case TYPE_INPUT_METHOD: { + return "TYPE_INPUT_METHOD"; + } + case TYPE_SYSTEM: { + return "TYPE_SYSTEM"; + } + default: + return "<UNKNOWN>"; + } + } + + /** + * Checks whether this window changed. The argument should be + * another state of the same window, which is have the same id + * and type as they never change. + * + * @param other The new state. + * @return Whether something changed. + * + * @hide + */ + public boolean changed(AccessibilityWindowInfo other) { + if (other.mId != mId) { + throw new IllegalArgumentException("Not same window."); + } + if (other.mType != mType) { + throw new IllegalArgumentException("Not same type."); + } + if (!mBoundsInScreen.equals(mBoundsInScreen)) { + return true; + } + if (mLayer != other.mLayer) { + return true; + } + if (mBooleanProperties != other.mBooleanProperties) { + return true; + } + if (mParentId != other.mParentId) { + return true; + } + if (mChildIds == null) { + if (other.mChildIds != null) { + return true; + } + } else if (!mChildIds.equals(other.mChildIds)) { + return true; + } + return false; + } + + public static final Parcelable.Creator<AccessibilityWindowInfo> CREATOR = + new Creator<AccessibilityWindowInfo>() { + @Override + public AccessibilityWindowInfo createFromParcel(Parcel parcel) { + AccessibilityWindowInfo info = obtain(); + info.initFromParcel(parcel); + return info; + } + + @Override + public AccessibilityWindowInfo[] newArray(int size) { + return new AccessibilityWindowInfo[size]; + } + }; +} diff --git a/core/java/android/view/accessibility/CaptioningManager.java b/core/java/android/view/accessibility/CaptioningManager.java index 557239f..a0134d6 100644 --- a/core/java/android/view/accessibility/CaptioningManager.java +++ b/core/java/android/view/accessibility/CaptioningManager.java @@ -250,6 +250,9 @@ public class CaptioningManager { * background colors, edge properties, and typeface. */ public static final class CaptionStyle { + /** Packed value for a color of 'none' and a cached opacity of 100%. */ + private static final int COLOR_NONE_OPAQUE = 0x000000FF; + private static final CaptionStyle WHITE_ON_BLACK; private static final CaptionStyle BLACK_ON_WHITE; private static final CaptionStyle YELLOW_ON_BLACK; @@ -271,6 +274,12 @@ public class CaptioningManager { /** Edge type value specifying drop-shadowed character edges. */ public static final int EDGE_TYPE_DROP_SHADOW = 2; + /** Edge type value specifying raised bevel character edges. */ + public static final int EDGE_TYPE_RAISED = 3; + + /** Edge type value specifying depressed bevel character edges. */ + public static final int EDGE_TYPE_DEPRESSED = 4; + /** The preferred foreground color for video captions. */ public final int foregroundColor; @@ -283,6 +292,8 @@ public class CaptioningManager { * <li>{@link #EDGE_TYPE_NONE} * <li>{@link #EDGE_TYPE_OUTLINE} * <li>{@link #EDGE_TYPE_DROP_SHADOW} + * <li>{@link #EDGE_TYPE_RAISED} + * <li>{@link #EDGE_TYPE_DEPRESSED} * </ul> */ public final int edgeType; @@ -293,6 +304,9 @@ public class CaptioningManager { */ public final int edgeColor; + /** The preferred window color for video captions. */ + public final int windowColor; + /** * @hide */ @@ -301,11 +315,12 @@ public class CaptioningManager { private Typeface mParsedTypeface; private CaptionStyle(int foregroundColor, int backgroundColor, int edgeType, int edgeColor, - String rawTypeface) { + int windowColor, String rawTypeface) { this.foregroundColor = foregroundColor; this.backgroundColor = backgroundColor; this.edgeType = edgeType; this.edgeColor = edgeColor; + this.windowColor = windowColor; mRawTypeface = rawTypeface; } @@ -334,25 +349,27 @@ public class CaptioningManager { cr, Secure.ACCESSIBILITY_CAPTIONING_EDGE_TYPE, defStyle.edgeType); final int edgeColor = Secure.getInt( cr, Secure.ACCESSIBILITY_CAPTIONING_EDGE_COLOR, defStyle.edgeColor); + final int windowColor = Secure.getInt( + cr, Secure.ACCESSIBILITY_CAPTIONING_WINDOW_COLOR, defStyle.windowColor); String rawTypeface = Secure.getString(cr, Secure.ACCESSIBILITY_CAPTIONING_TYPEFACE); if (rawTypeface == null) { rawTypeface = defStyle.mRawTypeface; } - return new CaptionStyle( - foregroundColor, backgroundColor, edgeType, edgeColor, rawTypeface); + return new CaptionStyle(foregroundColor, backgroundColor, edgeType, edgeColor, + windowColor, rawTypeface); } static { - WHITE_ON_BLACK = new CaptionStyle( - Color.WHITE, Color.BLACK, EDGE_TYPE_NONE, Color.BLACK, null); - BLACK_ON_WHITE = new CaptionStyle( - Color.BLACK, Color.WHITE, EDGE_TYPE_NONE, Color.BLACK, null); - YELLOW_ON_BLACK = new CaptionStyle( - Color.YELLOW, Color.BLACK, EDGE_TYPE_NONE, Color.BLACK, null); - YELLOW_ON_BLUE = new CaptionStyle( - Color.YELLOW, Color.BLUE, EDGE_TYPE_NONE, Color.BLACK, null); + WHITE_ON_BLACK = new CaptionStyle(Color.WHITE, Color.BLACK, EDGE_TYPE_NONE, + Color.BLACK, COLOR_NONE_OPAQUE, null); + BLACK_ON_WHITE = new CaptionStyle(Color.BLACK, Color.WHITE, EDGE_TYPE_NONE, + Color.BLACK, COLOR_NONE_OPAQUE, null); + YELLOW_ON_BLACK = new CaptionStyle(Color.YELLOW, Color.BLACK, EDGE_TYPE_NONE, + Color.BLACK, COLOR_NONE_OPAQUE, null); + YELLOW_ON_BLUE = new CaptionStyle(Color.YELLOW, Color.BLUE, EDGE_TYPE_NONE, + Color.BLACK, COLOR_NONE_OPAQUE, null); PRESETS = new CaptionStyle[] { WHITE_ON_BLACK, BLACK_ON_WHITE, YELLOW_ON_BLACK, YELLOW_ON_BLUE diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl index fe3e5c6..b6570cc 100644 --- a/core/java/android/view/accessibility/IAccessibilityManager.aidl +++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl @@ -57,4 +57,6 @@ interface IAccessibilityManager { void temporaryEnableAccessibilityStateUntilKeyguardRemoved(in ComponentName service, boolean touchExplorationEnabled); + + IBinder getWindowToken(int windowId); } diff --git a/core/java/android/view/animation/AnimationUtils.java b/core/java/android/view/animation/AnimationUtils.java index 38043b2..1d1fa1e 100644 --- a/core/java/android/view/animation/AnimationUtils.java +++ b/core/java/android/view/animation/AnimationUtils.java @@ -324,6 +324,8 @@ public class AnimationUtils { interpolator = new AnticipateOvershootInterpolator(c, attrs); } else if (name.equals("bounceInterpolator")) { interpolator = new BounceInterpolator(c, attrs); + } else if (name.equals("pathInterpolator")) { + interpolator = new PathInterpolator(c, attrs); } else { throw new RuntimeException("Unknown interpolator name: " + parser.getName()); } diff --git a/core/java/android/view/animation/BounceInterpolator.java b/core/java/android/view/animation/BounceInterpolator.java index f79e730..ecf99a7 100644 --- a/core/java/android/view/animation/BounceInterpolator.java +++ b/core/java/android/view/animation/BounceInterpolator.java @@ -17,7 +17,6 @@ package android.view.animation; import android.content.Context; -import android.content.res.TypedArray; import android.util.AttributeSet; /** diff --git a/core/java/android/view/animation/ClipRectAnimation.java b/core/java/android/view/animation/ClipRectAnimation.java new file mode 100644 index 0000000..2361501 --- /dev/null +++ b/core/java/android/view/animation/ClipRectAnimation.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2014 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.view.animation; + +import android.graphics.Rect; + +/** + * An animation that controls the clip of an object. See the + * {@link android.view.animation full package} description for details and + * sample code. + * + * @hide + */ +public class ClipRectAnimation extends Animation { + private Rect mFromRect = new Rect(); + private Rect mToRect = new Rect(); + + /** + * Constructor to use when building a ClipRectAnimation from code + * + * @param fromClip the clip rect to animate from + * @param toClip the clip rect to animate to + */ + public ClipRectAnimation(Rect fromClip, Rect toClip) { + if (fromClip == null || toClip == null) { + throw new RuntimeException("Expected non-null animation clip rects"); + } + mFromRect.set(fromClip); + mToRect.set(toClip); + } + + @Override + protected void applyTransformation(float it, Transformation tr) { + int l = mFromRect.left + (int) ((mToRect.left - mFromRect.left) * it); + int t = mFromRect.top + (int) ((mToRect.top - mFromRect.top) * it); + int r = mFromRect.right + (int) ((mToRect.right - mFromRect.right) * it); + int b = mFromRect.bottom + (int) ((mToRect.bottom - mFromRect.bottom) * it); + tr.setClipRect(l, t, r, b); + } + + @Override + public boolean willChangeTransformationMatrix() { + return false; + } +} diff --git a/core/java/android/view/animation/PathInterpolator.java b/core/java/android/view/animation/PathInterpolator.java new file mode 100644 index 0000000..a369509 --- /dev/null +++ b/core/java/android/view/animation/PathInterpolator.java @@ -0,0 +1,203 @@ +/* + * 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.view.animation; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Path; +import android.util.AttributeSet; +import android.view.InflateException; + +/** + * An interpolator that can traverse a Path that extends from <code>Point</code> + * <code>(0, 0)</code> to <code>(1, 1)</code>. The x coordinate along the <code>Path</code> + * is the input value and the output is the y coordinate of the line at that point. + * This means that the Path must conform to a function <code>y = f(x)</code>. + * + * <p>The <code>Path</code> must not have gaps in the x direction and must not + * loop back on itself such that there can be two points sharing the same x coordinate. + * It is alright to have a disjoint line in the vertical direction:</p> + * <p><blockquote><pre> + * Path path = new Path(); + * path.lineTo(0.25f, 0.25f); + * path.moveTo(0.25f, 0.5f); + * path.lineTo(1f, 1f); + * </pre></blockquote></p> + */ +public class PathInterpolator implements Interpolator { + + // This governs how accurate the approximation of the Path is. + private static final float PRECISION = 0.002f; + + private float[] mX; // x coordinates in the line + + private float[] mY; // y coordinates in the line + + /** + * Create an interpolator for an arbitrary <code>Path</code>. The <code>Path</code> + * must begin at <code>(0, 0)</code> and end at <code>(1, 1)</code>. + * + * @param path The <code>Path</code> to use to make the line representing the interpolator. + */ + public PathInterpolator(Path path) { + initPath(path); + } + + /** + * Create an interpolator for a quadratic Bezier curve. The end points + * <code>(0, 0)</code> and <code>(1, 1)</code> are assumed. + * + * @param controlX The x coordinate of the quadratic Bezier control point. + * @param controlY The y coordinate of the quadratic Bezier control point. + */ + public PathInterpolator(float controlX, float controlY) { + initQuad(controlX, controlY); + } + + /** + * Create an interpolator for a cubic Bezier curve. The end points + * <code>(0, 0)</code> and <code>(1, 1)</code> are assumed. + * + * @param controlX1 The x coordinate of the first control point of the cubic Bezier. + * @param controlY1 The y coordinate of the first control point of the cubic Bezier. + * @param controlX2 The x coordinate of the second control point of the cubic Bezier. + * @param controlY2 The y coordinate of the second control point of the cubic Bezier. + */ + public PathInterpolator(float controlX1, float controlY1, float controlX2, float controlY2) { + initCubic(controlX1, controlY1, controlX2, controlY2); + } + + public PathInterpolator(Context context, AttributeSet attrs) { + TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.PathInterpolator); + if (!a.hasValue(com.android.internal.R.styleable.PathInterpolator_controlX1)) { + throw new InflateException("pathInterpolator requires the controlX1 attribute"); + } else if (!a.hasValue(com.android.internal.R.styleable.PathInterpolator_controlY1)) { + throw new InflateException("pathInterpolator requires the controlY1 attribute"); + } + float x1 = a.getFloat(com.android.internal.R.styleable.PathInterpolator_controlX1, 0); + float y1 = a.getFloat(com.android.internal.R.styleable.PathInterpolator_controlY1, 0); + + boolean hasX2 = a.hasValue(com.android.internal.R.styleable.PathInterpolator_controlX2); + boolean hasY2 = a.hasValue(com.android.internal.R.styleable.PathInterpolator_controlY2); + + if (hasX2 != hasY2) { + throw new InflateException( + "pathInterpolator requires both controlX2 and controlY2 for cubic Beziers."); + } + + if (!hasX2) { + initQuad(x1, y1); + } else { + float x2 = a.getFloat(com.android.internal.R.styleable.PathInterpolator_controlX2, 0); + float y2 = a.getFloat(com.android.internal.R.styleable.PathInterpolator_controlY2, 0); + initCubic(x1, y1, x2, y2); + } + + a.recycle(); + } + + private void initQuad(float controlX, float controlY) { + Path path = new Path(); + path.moveTo(0, 0); + path.quadTo(controlX, controlY, 1f, 1f); + initPath(path); + } + + private void initCubic(float x1, float y1, float x2, float y2) { + Path path = new Path(); + path.moveTo(0, 0); + path.cubicTo(x1, y1, x2, y2, 1f, 1f); + initPath(path); + } + + private void initPath(Path path) { + float[] pointComponents = path.approximate(PRECISION); + + int numPoints = pointComponents.length / 3; + if (pointComponents[1] != 0 || pointComponents[2] != 0 + || pointComponents[pointComponents.length - 2] != 1 + || pointComponents[pointComponents.length - 1] != 1) { + throw new IllegalArgumentException("The Path must start at (0,0) and end at (1,1)"); + } + + mX = new float[numPoints]; + mY = new float[numPoints]; + float prevX = 0; + float prevFraction = 0; + int componentIndex = 0; + for (int i = 0; i < numPoints; i++) { + float fraction = pointComponents[componentIndex++]; + float x = pointComponents[componentIndex++]; + float y = pointComponents[componentIndex++]; + if (fraction == prevFraction && x != prevX) { + throw new IllegalArgumentException( + "The Path cannot have discontinuity in the X axis."); + } + if (x < prevX) { + throw new IllegalArgumentException("The Path cannot loop back on itself."); + } + mX[i] = x; + mY[i] = y; + prevX = x; + prevFraction = fraction; + } + } + + /** + * Using the line in the Path in this interpolator that can be described as + * <code>y = f(x)</code>, finds the y coordinate of the line given <code>t</code> + * as the x coordinate. Values less than 0 will always return 0 and values greater + * than 1 will always return 1. + * + * @param t Treated as the x coordinate along the line. + * @return The y coordinate of the Path along the line where x = <code>t</code>. + * @see Interpolator#getInterpolation(float) + */ + @Override + public float getInterpolation(float t) { + if (t <= 0) { + return 0; + } else if (t >= 1) { + return 1; + } + // Do a binary search for the correct x to interpolate between. + int startIndex = 0; + int endIndex = mX.length - 1; + + while (endIndex - startIndex > 1) { + int midIndex = (startIndex + endIndex) / 2; + if (t < mX[midIndex]) { + endIndex = midIndex; + } else { + startIndex = midIndex; + } + } + + float xRange = mX[endIndex] - mX[startIndex]; + if (xRange == 0) { + return mY[startIndex]; + } + + float tInRange = t - mX[startIndex]; + float fraction = tInRange / xRange; + + float startY = mY[startIndex]; + float endY = mY[endIndex]; + return startY + (fraction * (endY - startY)); + } + +} diff --git a/core/java/android/view/animation/Transformation.java b/core/java/android/view/animation/Transformation.java index 890909b..2f4fe73 100644 --- a/core/java/android/view/animation/Transformation.java +++ b/core/java/android/view/animation/Transformation.java @@ -17,6 +17,7 @@ package android.view.animation; import android.graphics.Matrix; +import android.graphics.Rect; import java.io.PrintWriter; @@ -47,6 +48,9 @@ public class Transformation { protected float mAlpha; protected int mTransformationType; + private boolean mHasClipRect; + private Rect mClipRect = new Rect(); + /** * Creates a new transformation with alpha = 1 and the identity matrix. */ @@ -65,6 +69,8 @@ public class Transformation { } else { mMatrix.reset(); } + mClipRect.setEmpty(); + mHasClipRect = false; mAlpha = 1.0f; mTransformationType = TYPE_BOTH; } @@ -98,9 +104,15 @@ public class Transformation { public void set(Transformation t) { mAlpha = t.getAlpha(); mMatrix.set(t.getMatrix()); + if (t.mHasClipRect) { + setClipRect(t.getClipRect()); + } else { + mHasClipRect = false; + mClipRect.setEmpty(); + } mTransformationType = t.getTransformationType(); } - + /** * Apply this Transformation to an existing Transformation, e.g. apply * a scale effect to something that has already been rotated. @@ -109,6 +121,9 @@ public class Transformation { public void compose(Transformation t) { mAlpha *= t.getAlpha(); mMatrix.preConcat(t.getMatrix()); + if (t.mHasClipRect) { + setClipRect(t.getClipRect()); + } } /** @@ -119,6 +134,9 @@ public class Transformation { public void postCompose(Transformation t) { mAlpha *= t.getAlpha(); mMatrix.postConcat(t.getMatrix()); + if (t.mHasClipRect) { + setClipRect(t.getClipRect()); + } } /** @@ -138,6 +156,39 @@ public class Transformation { } /** + * Sets the current Transform's clip rect + * @hide + */ + public void setClipRect(Rect r) { + setClipRect(r.left, r.top, r.right, r.bottom); + } + + /** + * Sets the current Transform's clip rect + * @hide + */ + public void setClipRect(int l, int t, int r, int b) { + mClipRect.set(l, t, r, b); + mHasClipRect = true; + } + + /** + * Returns the current Transform's clip rect + * @hide + */ + public Rect getClipRect() { + return mClipRect; + } + + /** + * Returns whether the current Transform's clip rect is set + * @hide + */ + public boolean hasClipRect() { + return mHasClipRect; + } + + /** * @return The degree of transparency */ public float getAlpha() { diff --git a/core/java/android/view/inputmethod/BaseInputConnection.java b/core/java/android/view/inputmethod/BaseInputConnection.java index f730cf7..cccfa78 100644 --- a/core/java/android/view/inputmethod/BaseInputConnection.java +++ b/core/java/android/view/inputmethod/BaseInputConnection.java @@ -484,10 +484,10 @@ public class BaseInputConnection implements InputConnection { final Editable content = getEditable(); if (content == null) return false; int len = content.length(); - if (start > len || end > len) { + if (start > len || end > len || start < 0 || end < 0) { // If the given selection is out of bounds, just ignore it. // Most likely the text was changed out from under the IME, - // the the IME is going to have to update all of its state + // and the IME is going to have to update all of its state // anyway. return true; } @@ -601,7 +601,11 @@ public class BaseInputConnection implements InputConnection { } beginBatchEdit(); - + if (!composing && !TextUtils.isEmpty(text)) { + // Notify the text is committed by the user to InputMethodManagerService + mIMM.notifyTextCommitted(); + } + // delete composing text set previously. int a = getComposingSpanStart(content); int b = getComposingSpanEnd(content); diff --git a/core/java/android/view/inputmethod/EditorInfo.java b/core/java/android/view/inputmethod/EditorInfo.java index d4e005b..c0395cf 100644 --- a/core/java/android/view/inputmethod/EditorInfo.java +++ b/core/java/android/view/inputmethod/EditorInfo.java @@ -26,13 +26,13 @@ import android.util.Printer; /** * An EditorInfo describes several attributes of a text editing object * that an input method is communicating with (typically an EditText), most - * importantly the type of text content it contains. + * importantly the type of text content it contains and the current cursor position. */ public class EditorInfo implements InputType, Parcelable { /** * The content type of the text box, whose bits are defined by * {@link InputType}. - * + * * @see InputType * @see #TYPE_MASK_CLASS * @see #TYPE_MASK_VARIATION @@ -47,55 +47,55 @@ public class EditorInfo implements InputType, Parcelable { * to provide alternative mechanisms for providing that command. */ public static final int IME_MASK_ACTION = 0x000000ff; - + /** * Bits of {@link #IME_MASK_ACTION}: no specific action has been * associated with this editor, let the editor come up with its own if * it can. */ public static final int IME_ACTION_UNSPECIFIED = 0x00000000; - + /** * Bits of {@link #IME_MASK_ACTION}: there is no available action. */ public static final int IME_ACTION_NONE = 0x00000001; - + /** * Bits of {@link #IME_MASK_ACTION}: the action key performs a "go" * operation to take the user to the target of the text they typed. * Typically used, for example, when entering a URL. */ public static final int IME_ACTION_GO = 0x00000002; - + /** * Bits of {@link #IME_MASK_ACTION}: the action key performs a "search" * operation, taking the user to the results of searching for the text * they have typed (in whatever context is appropriate). */ public static final int IME_ACTION_SEARCH = 0x00000003; - + /** * Bits of {@link #IME_MASK_ACTION}: the action key performs a "send" * operation, delivering the text to its target. This is typically used * when composing a message in IM or SMS where sending is immediate. */ public static final int IME_ACTION_SEND = 0x00000004; - + /** * Bits of {@link #IME_MASK_ACTION}: the action key performs a "next" * operation, taking the user to the next field that will accept text. */ public static final int IME_ACTION_NEXT = 0x00000005; - + /** * Bits of {@link #IME_MASK_ACTION}: the action key performs a "done" * operation, typically meaning there is nothing more to input and the * IME will be closed. */ public static final int IME_ACTION_DONE = 0x00000006; - + /** - * Bits of {@link #IME_MASK_ACTION}: Like {@link #IME_ACTION_NEXT}, but + * Bits of {@link #IME_MASK_ACTION}: like {@link #IME_ACTION_NEXT}, but * for moving to the previous field. This will normally not be used to * specify an action (since it precludes {@link #IME_ACTION_NEXT}), but * can be returned to the app if it sets {@link #IME_FLAG_NAVIGATE_PREVIOUS}. @@ -154,7 +154,7 @@ public class EditorInfo implements InputType, Parcelable { * on older versions of the platform. */ public static final int IME_FLAG_NO_EXTRACT_UI = 0x10000000; - + /** * Flag of {@link #imeOptions}: used in conjunction with one of the actions * masked by {@link #IME_MASK_ACTION}, this indicates that the action @@ -167,7 +167,7 @@ public class EditorInfo implements InputType, Parcelable { * to show more text. */ public static final int IME_FLAG_NO_ACCESSORY_ACTION = 0x20000000; - + /** * Flag of {@link #imeOptions}: used in conjunction with one of the actions * masked by {@link #IME_MASK_ACTION}. If this flag is not set, IMEs will @@ -202,13 +202,13 @@ public class EditorInfo implements InputType, Parcelable { * Generic unspecified type for {@link #imeOptions}. */ public static final int IME_NULL = 0x00000000; - + /** * Extended type information for the editor, to help the IME better * integrate with it. */ public int imeOptions = IME_NULL; - + /** * A string supplying additional information options that are * private to a particular IME implementation. The string must be @@ -221,7 +221,7 @@ public class EditorInfo implements InputType, Parcelable { * attribute of a TextView. */ public String privateImeOptions = null; - + /** * In some cases an IME may be able to display an arbitrary label for * a command the user can perform, which you can specify here. This is @@ -233,7 +233,7 @@ public class EditorInfo implements InputType, Parcelable { * ignore this. */ public CharSequence actionLabel = null; - + /** * If {@link #actionLabel} has been given, this is the id for that command * when the user presses its button that is delivered back with @@ -241,50 +241,66 @@ public class EditorInfo implements InputType, Parcelable { * InputConnection.performEditorAction()}. */ public int actionId = 0; - + /** * The text offset of the start of the selection at the time editing - * began; -1 if not known. Keep in mind some IMEs may not be able - * to give their full feature set without knowing the cursor position; - * avoid passing -1 here if you can. + * begins; -1 if not known. Keep in mind that, without knowing the cursor + * position, many IMEs will not be able to offer their full feature set and + * may even behave in unpredictable ways: pass the actual cursor position + * here if possible at all. + * + * <p>Also, this needs to be the cursor position <strong>right now</strong>, + * not at some point in the past, even if input is starting in the same text field + * as before. When the app is filling this object, input is about to start by + * definition, and this value will override any value the app may have passed to + * {@link InputMethodManager#updateSelection(android.view.View, int, int, int, int)} + * before.</p> */ public int initialSelStart = -1; - + /** - * The text offset of the end of the selection at the time editing - * began; -1 if not known. Keep in mind some IMEs may not be able - * to give their full feature set without knowing the cursor position; - * avoid passing -1 here if you can. + * <p>The text offset of the end of the selection at the time editing + * begins; -1 if not known. Keep in mind that, without knowing the cursor + * position, many IMEs will not be able to offer their full feature set and + * may behave in unpredictable ways: pass the actual cursor position + * here if possible at all.</p> + * + * <p>Also, this needs to be the cursor position <strong>right now</strong>, + * not at some point in the past, even if input is starting in the same text field + * as before. When the app is filling this object, input is about to start by + * definition, and this value will override any value the app may have passed to + * {@link InputMethodManager#updateSelection(android.view.View, int, int, int, int)} + * before.</p> */ public int initialSelEnd = -1; - + /** * The capitalization mode of the first character being edited in the * text. Values may be any combination of * {@link TextUtils#CAP_MODE_CHARACTERS TextUtils.CAP_MODE_CHARACTERS}, * {@link TextUtils#CAP_MODE_WORDS TextUtils.CAP_MODE_WORDS}, and * {@link TextUtils#CAP_MODE_SENTENCES TextUtils.CAP_MODE_SENTENCES}, though - * you should generally just take a non-zero value to mean start out in - * caps mode. + * you should generally just take a non-zero value to mean "start out in + * caps mode". */ public int initialCapsMode = 0; - + /** * The "hint" text of the text view, typically shown in-line when the * text is empty to tell the user what to enter. */ public CharSequence hintText; - + /** * A label to show to the user describing the text they are writing. */ public CharSequence label; - + /** * Name of the package that owns this editor. */ public String packageName; - + /** * Identifier for the editor's field. This is optional, and may be * 0. By default it is filled in with the result of @@ -292,14 +308,14 @@ public class EditorInfo implements InputType, Parcelable { * is being edited. */ public int fieldId; - + /** * Additional name for the editor's field. This can supply additional * name information for the field. By default it is null. The actual * contents have no meaning. */ public String fieldName; - + /** * Any extra data to supply to the input method. This is for extended * communication with specific input methods; the name fields in the @@ -309,7 +325,7 @@ public class EditorInfo implements InputType, Parcelable { * attribute of a TextView. */ public Bundle extras; - + /** * Ensure that the data in this EditorInfo is compatible with an application * that was developed against the given target API version. This can @@ -365,10 +381,10 @@ public class EditorInfo implements InputType, Parcelable { + " fieldName=" + fieldName); pw.println(prefix + "extras=" + extras); } - + /** * Used to package this object into a {@link Parcel}. - * + * * @param dest The {@link Parcel} to be written. * @param flags The flags used for parceling. */ @@ -392,30 +408,31 @@ public class EditorInfo implements InputType, Parcelable { /** * Used to make this class parcelable. */ - public static final Parcelable.Creator<EditorInfo> CREATOR = new Parcelable.Creator<EditorInfo>() { - public EditorInfo createFromParcel(Parcel source) { - EditorInfo res = new EditorInfo(); - res.inputType = source.readInt(); - res.imeOptions = source.readInt(); - res.privateImeOptions = source.readString(); - res.actionLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); - res.actionId = source.readInt(); - res.initialSelStart = source.readInt(); - res.initialSelEnd = source.readInt(); - res.initialCapsMode = source.readInt(); - res.hintText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); - res.label = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); - res.packageName = source.readString(); - res.fieldId = source.readInt(); - res.fieldName = source.readString(); - res.extras = source.readBundle(); - return res; - } + public static final Parcelable.Creator<EditorInfo> CREATOR = + new Parcelable.Creator<EditorInfo>() { + public EditorInfo createFromParcel(Parcel source) { + EditorInfo res = new EditorInfo(); + res.inputType = source.readInt(); + res.imeOptions = source.readInt(); + res.privateImeOptions = source.readString(); + res.actionLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); + res.actionId = source.readInt(); + res.initialSelStart = source.readInt(); + res.initialSelEnd = source.readInt(); + res.initialCapsMode = source.readInt(); + res.hintText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); + res.label = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); + res.packageName = source.readString(); + res.fieldId = source.readInt(); + res.fieldName = source.readString(); + res.extras = source.readBundle(); + return res; + } - public EditorInfo[] newArray(int size) { - return new EditorInfo[size]; - } - }; + public EditorInfo[] newArray(int size) { + return new EditorInfo[size]; + } + }; public int describeContents() { return 0; diff --git a/core/java/android/view/inputmethod/ExtractedTextRequest.java b/core/java/android/view/inputmethod/ExtractedTextRequest.java index f658b87..bf0bef3 100644 --- a/core/java/android/view/inputmethod/ExtractedTextRequest.java +++ b/core/java/android/view/inputmethod/ExtractedTextRequest.java @@ -18,7 +18,6 @@ package android.view.inputmethod; import android.os.Parcel; import android.os.Parcelable; -import android.text.TextUtils; /** * Description of what an input method would like from an application when diff --git a/core/java/android/view/inputmethod/InputBinding.java b/core/java/android/view/inputmethod/InputBinding.java index f4209ef..bcd459e 100644 --- a/core/java/android/view/inputmethod/InputBinding.java +++ b/core/java/android/view/inputmethod/InputBinding.java @@ -19,7 +19,6 @@ package android.view.inputmethod; import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; -import android.text.TextUtils; /** * Information given to an {@link InputMethod} about a client connecting diff --git a/core/java/android/view/inputmethod/InputMethodInfo.java b/core/java/android/view/inputmethod/InputMethodInfo.java index 5df5811..f8160c8 100644 --- a/core/java/android/view/inputmethod/InputMethodInfo.java +++ b/core/java/android/view/inputmethod/InputMethodInfo.java @@ -1,12 +1,12 @@ /* * Copyright (C) 2007-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 @@ -37,6 +37,7 @@ import android.util.Printer; import android.util.Slog; import android.util.Xml; import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder; +import android.view.inputmethod.InputMethodSubtypeArray; import java.io.IOException; import java.util.ArrayList; @@ -64,7 +65,7 @@ public final class InputMethodInfo implements Parcelable { * The Service that implements this input method component. */ final ResolveInfo mService; - + /** * The unique string Id to identify the input method. This is generated * from the input method component. @@ -86,9 +87,9 @@ public final class InputMethodInfo implements Parcelable { final int mIsDefaultResId; /** - * The array of the subtypes. + * An array-like container of the subtypes. */ - private final ArrayList<InputMethodSubtype> mSubtypes = new ArrayList<InputMethodSubtype>(); + private final InputMethodSubtypeArray mSubtypes; private final boolean mIsAuxIme; @@ -138,28 +139,29 @@ public final class InputMethodInfo implements Parcelable { int isDefaultResId = 0; XmlResourceParser parser = null; + final ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>(); try { parser = si.loadXmlMetaData(pm, InputMethod.SERVICE_META_DATA); if (parser == null) { throw new XmlPullParserException("No " + InputMethod.SERVICE_META_DATA + " meta-data"); } - + Resources res = pm.getResourcesForApplication(si.applicationInfo); - + AttributeSet attrs = Xml.asAttributeSet(parser); - + int type; while ((type=parser.next()) != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) { } - + String nodeName = parser.getName(); if (!"input-method".equals(nodeName)) { throw new XmlPullParserException( "Meta-data does not start with input-method tag"); } - + TypedArray sa = res.obtainAttributes(attrs, com.android.internal.R.styleable.InputMethod); settingsActivityComponent = sa.getString( @@ -206,7 +208,7 @@ public final class InputMethodInfo implements Parcelable { if (!subtype.isAuxiliary()) { isAuxIme = false; } - mSubtypes.add(subtype); + subtypes.add(subtype); } } } catch (NameNotFoundException e) { @@ -216,7 +218,7 @@ public final class InputMethodInfo implements Parcelable { if (parser != null) parser.close(); } - if (mSubtypes.size() == 0) { + if (subtypes.size() == 0) { isAuxIme = false; } @@ -225,14 +227,15 @@ public final class InputMethodInfo implements Parcelable { final int N = additionalSubtypes.size(); for (int i = 0; i < N; ++i) { final InputMethodSubtype subtype = additionalSubtypes.get(i); - if (!mSubtypes.contains(subtype)) { - mSubtypes.add(subtype); + if (!subtypes.contains(subtype)) { + subtypes.add(subtype); } else { Slog.w(TAG, "Duplicated subtype definition found: " + subtype.getLocale() + ", " + subtype.getMode()); } } } + mSubtypes = new InputMethodSubtypeArray(subtypes); mSettingsActivityName = settingsActivityComponent; mIsDefaultResId = isDefaultResId; mIsAuxIme = isAuxIme; @@ -246,7 +249,7 @@ public final class InputMethodInfo implements Parcelable { mIsAuxIme = source.readInt() == 1; mSupportsSwitchingToNextInputMethod = source.readInt() == 1; mService = ResolveInfo.CREATOR.createFromParcel(source); - source.readTypedList(mSubtypes, InputMethodSubtype.CREATOR); + mSubtypes = new InputMethodSubtypeArray(source); mForceDefault = false; } @@ -272,9 +275,7 @@ public final class InputMethodInfo implements Parcelable { mSettingsActivityName = settingsActivity; mIsDefaultResId = isDefaultResId; mIsAuxIme = isAuxIme; - if (subtypes != null) { - mSubtypes.addAll(subtypes); - } + mSubtypes = new InputMethodSubtypeArray(subtypes); mForceDefault = forceDefault; mSupportsSwitchingToNextInputMethod = true; } @@ -338,7 +339,7 @@ public final class InputMethodInfo implements Parcelable { /** * Load the user-displayed label for this input method. - * + * * @param pm Supply a PackageManager used to load the input method's * resources. */ @@ -348,7 +349,7 @@ public final class InputMethodInfo implements Parcelable { /** * Load the user-displayed icon for this input method. - * + * * @param pm Supply a PackageManager used to load the input method's * resources. */ @@ -362,9 +363,9 @@ public final class InputMethodInfo implements Parcelable { * an {@link android.content.Intent} whose action is MAIN and with an * explicit {@link android.content.ComponentName} * composed of {@link #getPackageName} and the class name returned here. - * + * * <p>A null will be returned if there is no settings activity associated - * with the input method. + * with the input method.</p> */ public String getSettingsActivity() { return mSettingsActivityName; @@ -374,7 +375,7 @@ public final class InputMethodInfo implements Parcelable { * Return the count of the subtypes of Input Method. */ public int getSubtypeCount() { - return mSubtypes.size(); + return mSubtypes.getCount(); } /** @@ -419,7 +420,7 @@ public final class InputMethodInfo implements Parcelable { pw.println(prefix + "Service:"); mService.dump(pw, prefix + " "); } - + @Override public String toString() { return "InputMethodInfo{" + mId @@ -430,7 +431,7 @@ public final class InputMethodInfo implements Parcelable { /** * Used to test whether the given parameter object is an * {@link InputMethodInfo} and its Id is the same to this one. - * + * * @return true if the given parameter object is an * {@link InputMethodInfo} and its Id is the same to this one. */ @@ -467,7 +468,7 @@ public final class InputMethodInfo implements Parcelable { /** * Used to package this object into a {@link Parcel}. - * + * * @param dest The {@link Parcel} to be written. * @param flags The flags used for parceling. */ @@ -479,7 +480,7 @@ public final class InputMethodInfo implements Parcelable { dest.writeInt(mIsAuxIme ? 1 : 0); dest.writeInt(mSupportsSwitchingToNextInputMethod ? 1 : 0); mService.writeToParcel(dest, flags); - dest.writeTypedList(mSubtypes); + mSubtypes.writeToParcel(dest); } /** diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 53f7c79..e3bd9fd 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -26,6 +26,7 @@ import com.android.internal.view.InputBindResult; import android.content.Context; import android.graphics.Rect; +import android.inputmethodservice.InputMethodService; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; @@ -41,13 +42,13 @@ import android.util.Pools.Pool; import android.util.Pools.SimplePool; import android.util.PrintWriterPrinter; import android.util.Printer; +import android.util.SparseArray; import android.view.InputChannel; import android.view.InputEvent; import android.view.InputEventSender; import android.view.KeyEvent; import android.view.View; import android.view.ViewRootImpl; -import android.util.SparseArray; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -334,6 +335,11 @@ public final class InputMethodManager { InputChannel mCurChannel; ImeInputEventSender mCurSender; + /** + * The current cursor/anchor monitor mode. + */ + int mCursorAnchorMonitorMode = InputMethodService.CURSOR_ANCHOR_MONITOR_MODE_NONE; + final Pool<PendingEvent> mPendingEventPool = new SimplePool<PendingEvent>(20); final SparseArray<PendingEvent> mPendingEvents = new SparseArray<PendingEvent>(20); @@ -346,6 +352,7 @@ public final class InputMethodManager { static final int MSG_SEND_INPUT_EVENT = 5; static final int MSG_TIMEOUT_INPUT_EVENT = 6; static final int MSG_FLUSH_INPUT_EVENT = 7; + static final int SET_CURSOR_ANCHOR_MONITOR_MODE = 8; class H extends Handler { H(Looper looper) { @@ -476,6 +483,12 @@ public final class InputMethodManager { finishedInputEvent(msg.arg1, false, false); return; } + case SET_CURSOR_ANCHOR_MONITOR_MODE: { + synchronized (mH) { + mCursorAnchorMonitorMode = msg.arg1; + } + return; + } } } } @@ -540,6 +553,11 @@ public final class InputMethodManager { public void setActive(boolean active) { mH.sendMessage(mH.obtainMessage(MSG_SET_ACTIVE, active ? 1 : 0, 0)); } + + @Override + public void setCursorAnchorMonitorMode(int monitorMode) { + mH.sendMessage(mH.obtainMessage(SET_CURSOR_ANCHOR_MONITOR_MODE, monitorMode, 0)); + } }; final InputConnection mDummyInputConnection = new BaseInputConnection(this, false); @@ -714,6 +732,7 @@ public final class InputMethodManager { * Reset all of the state associated with being bound to an input method. */ void clearBindingLocked() { + if (DEBUG) Log.v(TAG, "Clearing binding!"); clearConnectionLocked(); setInputChannelLocked(null); mBindSequence = -1; @@ -1394,6 +1413,14 @@ public final class InputMethodManager { /** * Report the current selection range. + * + * <p><strong>Editor authors</strong>, you need to call this method whenever + * the cursor moves in your editor. Remember that in addition to doing this, your + * editor needs to always supply current cursor values in + * {@link EditorInfo#initialSelStart} and {@link EditorInfo#initialSelEnd} every + * time {@link android.view.View#onCreateInputConnection(EditorInfo)} is + * called, which happens whenever the keyboard shows up or the focus changes + * to a text field, among other cases.</p> */ public void updateSelection(View view, int selStart, int selEnd, int candidatesStart, int candidatesEnd) { @@ -1456,9 +1483,30 @@ public final class InputMethodManager { * of the input editor's cursor in its window. */ public boolean isWatchingCursor(View view) { - return false; + if (!isActive(view)) { + return false; + } + synchronized (mH) { + return mCursorAnchorMonitorMode == + InputMethodService.CURSOR_ANCHOR_MONITOR_MODE_CURSOR_RECT; + } } - + + /** + * Set cursor/anchor monitor mode via {@link com.android.server.InputMethodManagerService}. + * This is an internal method for {@link android.inputmethodservice.InputMethodService} and + * should never be used from IMEs and applications. + * + * @hide + */ + public void setCursorAnchorMonitorMode(IBinder imeToken, int monitorMode) { + try { + mService.setCursorAnchorMonitorMode(imeToken, monitorMode); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + /** * Report the current cursor location in its window. */ @@ -1805,6 +1853,20 @@ public final class InputMethodManager { } /** + * Notify the current IME commits text + * @hide + */ + public void notifyTextCommitted() { + synchronized (mH) { + try { + mService.notifyTextCommitted(); + } catch (RemoteException e) { + Log.w(TAG, "IME died: " + mCurId, e); + } + } + } + + /** * Returns a map of all shortcut input method info and their subtypes. */ public Map<InputMethodInfo, List<InputMethodSubtype>> getShortcutInputMethodsAndSubtypes() { @@ -1840,6 +1902,21 @@ public final class InputMethodManager { } /** + * @return The current height of the input method window. + * @hide + */ + public int getInputMethodWindowVisibleHeight() { + synchronized (mH) { + try { + return mService.getInputMethodWindowVisibleHeight(); + } catch (RemoteException e) { + Log.w(TAG, "IME died: " + mCurId, e); + return 0; + } + } + } + + /** * Force switch to the last used input method and subtype. If the last input method didn't have * any subtypes, the framework will simply switch to the last input method with no subtype * specified. diff --git a/core/java/android/view/inputmethod/InputMethodSubtype.java b/core/java/android/view/inputmethod/InputMethodSubtype.java index 2ab3024..e7ada27 100644 --- a/core/java/android/view/inputmethod/InputMethodSubtype.java +++ b/core/java/android/view/inputmethod/InputMethodSubtype.java @@ -472,12 +472,12 @@ public final class InputMethodSubtype implements Parcelable { return (subtype.hashCode() == hashCode()); } return (subtype.hashCode() == hashCode()) - && (subtype.getNameResId() == getNameResId()) - && (subtype.getMode().equals(getMode())) - && (subtype.getIconResId() == getIconResId()) && (subtype.getLocale().equals(getLocale())) + && (subtype.getMode().equals(getMode())) && (subtype.getExtraValue().equals(getExtraValue())) && (subtype.isAuxiliary() == isAuxiliary()) + && (subtype.overridesImplicitlyEnabledSubtype() + == overridesImplicitlyEnabledSubtype()) && (subtype.isAsciiCapable() == isAsciiCapable()); } return false; diff --git a/core/java/android/view/inputmethod/InputMethodSubtypeArray.java b/core/java/android/view/inputmethod/InputMethodSubtypeArray.java new file mode 100644 index 0000000..5bef71f --- /dev/null +++ b/core/java/android/view/inputmethod/InputMethodSubtypeArray.java @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2007-2014 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.view.inputmethod; + +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AndroidRuntimeException; +import android.util.Slog; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +/** + * An array-like container that stores multiple instances of {@link InputMethodSubtype}. + * + * <p>This container is designed to reduce the risk of {@link TransactionTooLargeException} + * when one or more instancess of {@link InputMethodInfo} are transferred through IPC. + * Basically this class does following three tasks.</p> + * <ul> + * <li>Applying compression for the marshalled data</li> + * <li>Lazily unmarshalling objects</li> + * <li>Caching the marshalled data when appropriate</li> + * </ul> + * + * @hide + */ +public class InputMethodSubtypeArray { + private final static String TAG = "InputMethodSubtypeArray"; + + /** + * Create a new instance of {@link InputMethodSubtypeArray} from an existing list of + * {@link InputMethodSubtype}. + * + * @param subtypes A list of {@link InputMethodSubtype} from which + * {@link InputMethodSubtypeArray} will be created. + */ + public InputMethodSubtypeArray(final List<InputMethodSubtype> subtypes) { + if (subtypes == null) { + mCount = 0; + return; + } + mCount = subtypes.size(); + mInstance = subtypes.toArray(new InputMethodSubtype[mCount]); + } + + /** + * Unmarshall an instance of {@link InputMethodSubtypeArray} from a given {@link Parcel} + * object. + * + * @param source A {@link Parcel} object from which {@link InputMethodSubtypeArray} will be + * unmarshalled. + */ + public InputMethodSubtypeArray(final Parcel source) { + mCount = source.readInt(); + if (mCount > 0) { + mDecompressedSize = source.readInt(); + mCompressedData = source.createByteArray(); + } + } + + /** + * Marshall the instance into a given {@link Parcel} object. + * + * <p>This methods may take a bit additional time to compress data lazily when called + * first time.</p> + * + * @param source A {@link Parcel} object to which {@link InputMethodSubtypeArray} will be + * marshalled. + */ + public void writeToParcel(final Parcel dest) { + if (mCount == 0) { + dest.writeInt(mCount); + return; + } + + byte[] compressedData = mCompressedData; + int decompressedSize = mDecompressedSize; + if (compressedData == null && decompressedSize == 0) { + synchronized (mLockObject) { + compressedData = mCompressedData; + decompressedSize = mDecompressedSize; + if (compressedData == null && decompressedSize == 0) { + final byte[] decompressedData = marshall(mInstance); + compressedData = compress(decompressedData); + if (compressedData == null) { + decompressedSize = -1; + Slog.i(TAG, "Failed to compress data."); + } else { + decompressedSize = decompressedData.length; + } + mDecompressedSize = decompressedSize; + mCompressedData = compressedData; + } + } + } + + if (compressedData != null && decompressedSize > 0) { + dest.writeInt(mCount); + dest.writeInt(decompressedSize); + dest.writeByteArray(compressedData); + } else { + Slog.i(TAG, "Unexpected state. Behaving as an empty array."); + dest.writeInt(0); + } + } + + /** + * Return {@link InputMethodSubtype} specified with the given index. + * + * <p>This methods may take a bit additional time to decompress data lazily when called + * first time.</p> + * + * @param index The index of {@link InputMethodSubtype}. + */ + public InputMethodSubtype get(final int index) { + if (index < 0 || mCount <= index) { + throw new ArrayIndexOutOfBoundsException(); + } + InputMethodSubtype[] instance = mInstance; + if (instance == null) { + synchronized (mLockObject) { + instance = mInstance; + if (instance == null) { + final byte[] decompressedData = + decompress(mCompressedData, mDecompressedSize); + // Clear the compressed data until {@link #getMarshalled()} is called. + mCompressedData = null; + mDecompressedSize = 0; + if (decompressedData != null) { + instance = unmarshall(decompressedData); + } else { + Slog.e(TAG, "Failed to decompress data. Returns null as fallback."); + instance = new InputMethodSubtype[mCount]; + } + mInstance = instance; + } + } + } + return instance[index]; + } + + /** + * Return the number of {@link InputMethodSubtype} objects. + */ + public int getCount() { + return mCount; + } + + private final Object mLockObject = new Object(); + private final int mCount; + + private volatile InputMethodSubtype[] mInstance; + private volatile byte[] mCompressedData; + private volatile int mDecompressedSize; + + private static byte[] marshall(final InputMethodSubtype[] array) { + Parcel parcel = null; + try { + parcel = Parcel.obtain(); + parcel.writeTypedArray(array, 0); + return parcel.marshall(); + } finally { + if (parcel != null) { + parcel.recycle(); + parcel = null; + } + } + } + + private static InputMethodSubtype[] unmarshall(final byte[] data) { + Parcel parcel = null; + try { + parcel = Parcel.obtain(); + parcel.unmarshall(data, 0, data.length); + parcel.setDataPosition(0); + return parcel.createTypedArray(InputMethodSubtype.CREATOR); + } finally { + if (parcel != null) { + parcel.recycle(); + parcel = null; + } + } + } + + private static byte[] compress(final byte[] data) { + ByteArrayOutputStream resultStream = null; + GZIPOutputStream zipper = null; + try { + resultStream = new ByteArrayOutputStream(); + zipper = new GZIPOutputStream(resultStream); + zipper.write(data); + } catch(IOException e) { + return null; + } finally { + try { + if (zipper != null) { + zipper.close(); + } + } catch (IOException e) { + zipper = null; + Slog.e(TAG, "Failed to close the stream.", e); + // swallowed, not propagated back to the caller + } + try { + if (resultStream != null) { + resultStream.close(); + } + } catch (IOException e) { + resultStream = null; + Slog.e(TAG, "Failed to close the stream.", e); + // swallowed, not propagated back to the caller + } + } + return resultStream != null ? resultStream.toByteArray() : null; + } + + private static byte[] decompress(final byte[] data, final int expectedSize) { + ByteArrayInputStream inputStream = null; + GZIPInputStream unzipper = null; + try { + inputStream = new ByteArrayInputStream(data); + unzipper = new GZIPInputStream(inputStream); + final byte [] result = new byte[expectedSize]; + int totalReadBytes = 0; + while (totalReadBytes < result.length) { + final int restBytes = result.length - totalReadBytes; + final int readBytes = unzipper.read(result, totalReadBytes, restBytes); + if (readBytes < 0) { + break; + } + totalReadBytes += readBytes; + } + if (expectedSize != totalReadBytes) { + return null; + } + return result; + } catch(IOException e) { + return null; + } finally { + try { + if (unzipper != null) { + unzipper.close(); + } + } catch (IOException e) { + Slog.e(TAG, "Failed to close the stream.", e); + // swallowed, not propagated back to the caller + } + try { + if (inputStream != null) { + inputStream.close(); + } + } catch (IOException e) { + Slog.e(TAG, "Failed to close the stream.", e); + // swallowed, not propagated back to the caller + } + } + } +} diff --git a/core/java/android/webkit/CacheManager.java b/core/java/android/webkit/CacheManager.java index bbd3f2b..45e6eb3 100644 --- a/core/java/android/webkit/CacheManager.java +++ b/core/java/android/webkit/CacheManager.java @@ -16,13 +16,7 @@ package android.webkit; -import android.content.Context; -import android.net.http.Headers; -import android.util.Log; - import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; diff --git a/core/java/android/webkit/DateSorter.java b/core/java/android/webkit/DateSorter.java index 82c13ae..fede244 100644 --- a/core/java/android/webkit/DateSorter.java +++ b/core/java/android/webkit/DateSorter.java @@ -20,7 +20,6 @@ import android.content.Context; import android.content.res.Resources; import java.util.Calendar; -import java.util.Date; import java.util.Locale; import libcore.icu.LocaleData; diff --git a/core/java/android/webkit/DebugFlags.java b/core/java/android/webkit/DebugFlags.java index b5ca8c1..7b3cb1b 100644 --- a/core/java/android/webkit/DebugFlags.java +++ b/core/java/android/webkit/DebugFlags.java @@ -31,7 +31,6 @@ 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; diff --git a/core/java/android/webkit/PermissionRequest.java b/core/java/android/webkit/PermissionRequest.java new file mode 100644 index 0000000..2f8850b --- /dev/null +++ b/core/java/android/webkit/PermissionRequest.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit; + +import android.net.Uri; + +/** + * This class wraps a permission request, and is used to request permission for + * the web content to access the resources. + * + * Either {@link #grant(long) grant()} or {@link #deny()} must be called to response the + * request, otherwise, {@link WebChromeClient#onPermissionRequest(PermissionRequest)} will + * not be invoked again if there is other permission request in this WebView. + * + * @hide + */ +public interface PermissionRequest { + /** + * Resource belongs to geolocation service. + */ + public final static long RESOURCE_GEOLOCATION = 1 << 0; + /** + * Resource belongs to video capture device, like camera. + */ + public final static long RESOURCE_VIDEO_CAPTURE = 1 << 1; + /** + * Resource belongs to audio capture device, like microphone. + */ + public final static long RESOURCE_AUDIO_CAPTURE = 1 << 2; + + /** + * @return the origin of web content which attempt to access the restricted + * resources. + */ + public Uri getOrigin(); + + /** + * @return a bit mask of resources the web content wants to access. + */ + public long getResources(); + + /** + * Call this method to grant origin the permission to access the given resources. + * The granted permission is only valid for this WebView. + * + * @param resources the resources granted to be accessed by origin, to grant + * request, the requested resources returned by {@link #getResources()} + * must be equals or a subset of granted resources. + * This parameter is designed to avoid granting permission by accident + * especially when new resources are requested by web content. + * Calling grant(getResources()) has security issue, the new permission + * will be granted without being noticed. + */ + public void grant(long resources); + + /** + * Call this method to deny the request. + */ + public void deny(); +} diff --git a/core/java/android/webkit/Plugin.java b/core/java/android/webkit/Plugin.java index 529820b..072e02a 100644 --- a/core/java/android/webkit/Plugin.java +++ b/core/java/android/webkit/Plugin.java @@ -21,7 +21,6 @@ import com.android.internal.R; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; -import android.webkit.WebView; /** * Represents a plugin (Java equivalent of the PluginPackageAndroid diff --git a/core/java/android/webkit/WebChromeClient.java b/core/java/android/webkit/WebChromeClient.java index aa57423..60cba86 100644 --- a/core/java/android/webkit/WebChromeClient.java +++ b/core/java/android/webkit/WebChromeClient.java @@ -296,6 +296,30 @@ public class WebChromeClient { public void onGeolocationPermissionsHidePrompt() {} /** + * Notify the host application that web content is requesting permission to + * access the specified resources and the permission currently isn't granted + * or denied. The host application must invoke {@link PermissionRequest#grant(long)} + * or {@link PermissionRequest#deny()}. + * + * If this method isn't overridden, the permission is denied. + * + * @param request the PermissionRequest from current web content. + * @hide + */ + public void onPermissionRequest(PermissionRequest request) { + request.deny(); + } + + /** + * Notify the host application that the given permission request + * has been canceled. Any related UI should therefore be hidden. + * + * @param request the PermissionRequest need be canceled. + * @hide + */ + public void onPermissionRequestCanceled(PermissionRequest request) {} + + /** * Tell the client that a JavaScript execution timeout has occured. And the * client may decide whether or not to interrupt the execution. If the * client returns true, the JavaScript will be interrupted. If the client diff --git a/core/java/android/webkit/WebResourceResponse.java b/core/java/android/webkit/WebResourceResponse.java index b7171ee..f21e2b4 100644 --- a/core/java/android/webkit/WebResourceResponse.java +++ b/core/java/android/webkit/WebResourceResponse.java @@ -16,8 +16,6 @@ package android.webkit; -import android.net.http.Headers; - import java.io.InputStream; /** diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index d53bb74..62fbbc4 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -25,10 +25,10 @@ import android.graphics.Paint; import android.graphics.Picture; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.net.Uri; import android.net.http.SslCertificate; import android.os.Build; import android.os.Bundle; -import android.os.CancellationSignal; import android.os.Looper; import android.os.Message; import android.os.StrictMode; @@ -255,7 +255,7 @@ public class WebView extends AbsoluteLayout // Throwing an exception for incorrect thread usage if the // build target is JB MR2 or newer. Defaults to false, and is // set in the WebView constructor. - private static Boolean sEnforceThreadChecking = false; + private static volatile boolean sEnforceThreadChecking = false; /** * Transportation object for returning WebView across thread boundaries. @@ -449,10 +449,12 @@ public class WebView extends AbsoluteLayout * * @param context a Context object used to access application assets * @param attrs an AttributeSet passed to our parent - * @param defStyle the default style resource ID + * @param defStyleAttr an attribute in the current theme that contains a + * reference to a style resource that supplies default values for + * the view. Can be 0 to not look for defaults. */ - public WebView(Context context, AttributeSet attrs, int defStyle) { - this(context, attrs, defStyle, false); + public WebView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); } /** @@ -460,7 +462,26 @@ public class WebView extends AbsoluteLayout * * @param context a Context object used to access application assets * @param attrs an AttributeSet passed to our parent - * @param defStyle the default style resource ID + * @param defStyleAttr an attribute in the current theme that contains a + * reference to a style resource that supplies default values for + * the view. Can be 0 to not look for defaults. + * @param defStyleRes a resource identifier of a style resource that + * supplies default values for the view, used only if + * defStyleAttr is 0 or can not be found in the theme. Can be 0 + * to not look for defaults. + */ + public WebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + this(context, attrs, defStyleAttr, defStyleRes, null, false); + } + + /** + * Constructs a new WebView with layout parameters and a default style. + * + * @param context a Context object used to access application assets + * @param attrs an AttributeSet passed to our parent + * @param defStyleAttr an attribute in the current theme that contains a + * reference to a style resource that supplies default values for + * the view. Can be 0 to not look for defaults. * @param privateBrowsing whether this WebView will be initialized in * private mode * @@ -470,9 +491,9 @@ public class WebView extends AbsoluteLayout * and {@link WebStorage} for fine-grained control of privacy data. */ @Deprecated - public WebView(Context context, AttributeSet attrs, int defStyle, + public WebView(Context context, AttributeSet attrs, int defStyleAttr, boolean privateBrowsing) { - this(context, attrs, defStyle, null, privateBrowsing); + this(context, attrs, defStyleAttr, 0, null, privateBrowsing); } /** @@ -483,7 +504,9 @@ public class WebView extends AbsoluteLayout * * @param context a Context object used to access application assets * @param attrs an AttributeSet passed to our parent - * @param defStyle the default style resource ID + * @param defStyleAttr an attribute in the current theme that contains a + * reference to a style resource that supplies default values for + * the view. Can be 0 to not look for defaults. * @param javaScriptInterfaces a Map of interface names, as keys, and * object implementing those interfaces, as * values @@ -492,10 +515,18 @@ public class WebView extends AbsoluteLayout * @hide This is used internally by dumprendertree, as it requires the javaScript interfaces to * be added synchronously, before a subsequent loadUrl call takes effect. */ + protected WebView(Context context, AttributeSet attrs, int defStyleAttr, + Map<String, Object> javaScriptInterfaces, boolean privateBrowsing) { + this(context, attrs, defStyleAttr, 0, javaScriptInterfaces, privateBrowsing); + } + + /** + * @hide + */ @SuppressWarnings("deprecation") // for super() call into deprecated base class constructor. - protected WebView(Context context, AttributeSet attrs, int defStyle, + protected WebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes, Map<String, Object> javaScriptInterfaces, boolean privateBrowsing) { - super(context, attrs, defStyle); + super(context, attrs, defStyleAttr, defStyleRes); if (context == null) { throw new IllegalArgumentException("Invalid context argument"); } @@ -790,7 +821,15 @@ 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); + if (DebugFlags.TRACE_API) { + StringBuilder headers = new StringBuilder(); + if (additionalHttpHeaders != null) { + for (Map.Entry<String, String> entry : additionalHttpHeaders.entrySet()) { + headers.append(entry.getKey() + ":" + entry.getValue() + "\n"); + } + } + Log.d(LOGTAG, "loadUrl(extra headers)=" + url + "\n" + headers); + } mProvider.loadUrl(url, additionalHttpHeaders); } @@ -807,8 +846,8 @@ public class WebView extends AbsoluteLayout /** * Loads the URL with postData using "POST" method into this WebView. If url - * is not a network URL, it will be loaded with {link - * {@link #loadUrl(String)} instead. + * is not a network URL, it will be loaded with {@link #loadUrl(String)} + * instead, ignoring the postData param. * * @param url the URL of the resource to load * @param postData the data will be passed to "POST" request, which must be @@ -817,7 +856,11 @@ 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); + if (URLUtil.isNetworkUrl(url)) { + mProvider.postUrl(url, postData); + } else { + mProvider.loadUrl(url); + } } /** @@ -1598,6 +1641,21 @@ public class WebView extends AbsoluteLayout } /** + * Preauthorize the given origin to access resources. + * This authorization only valid for this WebView instance life cycle and + * will not retained. + * + * @param origin the origin authorized to access resources + * @param resources the resource authorized to be accessed by origin. + * + * @hide + */ + public void preauthorizePermission(Uri origin, long resources) { + checkThread(); + mProvider.preauthorizePermission(origin, resources); + } + + /** * Sets the Picture listener. This is an interface used to receive * notifications of a new Picture. * @@ -1649,6 +1707,9 @@ public class WebView extends AbsoluteLayout * thread of this WebView. Care is therefore required to maintain thread * safety.</li> * <li> The Java object's fields are not accessible.</li> + * <li> For applications targeted to API level {@link android.os.Build.VERSION_CODES#L} + * and above, methods of injected Java objects are enumerable from + * JavaScript.</li> * </ul> * * @param object the Java object to inject into this WebView's JavaScript @@ -2097,10 +2158,11 @@ public class WebView extends AbsoluteLayout mProvider.getViewDelegate().onAttachedToWindow(); } + /** @hide */ @Override - protected void onDetachedFromWindow() { + protected void onDetachedFromWindowInternal() { mProvider.getViewDelegate().onDetachedFromWindow(); - super.onDetachedFromWindow(); + super.onDetachedFromWindowInternal(); } @Override diff --git a/core/java/android/webkit/WebViewFactory.java b/core/java/android/webkit/WebViewFactory.java index b9131bf..25bcd44 100644 --- a/core/java/android/webkit/WebViewFactory.java +++ b/core/java/android/webkit/WebViewFactory.java @@ -16,9 +16,7 @@ package android.webkit; -import android.os.Build; import android.os.StrictMode; -import android.os.SystemProperties; import android.util.AndroidRuntimeException; import android.util.Log; diff --git a/core/java/android/webkit/WebViewProvider.java b/core/java/android/webkit/WebViewProvider.java index 696aad4..9488cdd 100644 --- a/core/java/android/webkit/WebViewProvider.java +++ b/core/java/android/webkit/WebViewProvider.java @@ -23,6 +23,7 @@ import android.graphics.Paint; import android.graphics.Picture; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.net.Uri; import android.net.http.SslCertificate; import android.os.Bundle; import android.os.Message; @@ -245,6 +246,8 @@ public interface WebViewProvider { public View findHierarchyView(String className, int hashCode); + public void preauthorizePermission(Uri origin, long resources); + //------------------------------------------------------------------------- // Provider internal methods //------------------------------------------------------------------------- diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index bbaa33d..301317e 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -36,6 +36,7 @@ import android.text.TextWatcher; import android.util.AttributeSet; import android.util.Log; import android.util.LongSparseArray; +import android.util.MathUtils; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.StateSet; @@ -59,6 +60,9 @@ import android.view.ViewTreeObserver; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeInfo.CollectionInfo; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; import android.view.inputmethod.BaseInputConnection; @@ -417,7 +421,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te /** * Handles scrolling between positions within the list. */ - PositionScroller mPositionScroller; + AbsPositionScroller mPositionScroller; /** * The offset in pixels form the top of the AdapterView to the top @@ -581,7 +585,13 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te /** * Helper object that renders and controls the fast scroll thumb. */ - private FastScroller mFastScroller; + private FastScroller mFastScroll; + + /** + * Temporary holder for fast scroller style until a FastScroller object + * is created. + */ + private int mFastScrollStyle; private boolean mGlobalLayoutListenerAddedFilter; @@ -693,6 +703,11 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te private SavedState mPendingSync; /** + * Whether the view is in the process of detaching from its window. + */ + private boolean mIsDetaching; + + /** * Interface definition for a callback to be invoked when the list or grid * has been scrolled. */ @@ -773,14 +788,18 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te this(context, attrs, com.android.internal.R.attr.absListViewStyle); } - public AbsListView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public AbsListView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public AbsListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); initAbsListView(); mOwnerThread = Thread.currentThread(); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.AbsListView, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.AbsListView, defStyleAttr, defStyleRes); Drawable d = a.getDrawable(com.android.internal.R.styleable.AbsListView_listSelector); if (d != null) { @@ -809,6 +828,9 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te boolean enableFastScroll = a.getBoolean(R.styleable.AbsListView_fastScrollEnabled, false); setFastScrollEnabled(enableFastScroll); + int fastScrollStyle = a.getResourceId(R.styleable.AbsListView_fastScrollStyle, 0); + setFastScrollStyle(fastScrollStyle); + boolean smoothScrollbar = a.getBoolean(R.styleable.AbsListView_smoothScrollbar, true); setSmoothScrollbarEnabled(smoothScrollbar); @@ -1238,17 +1260,31 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } private void setFastScrollerEnabledUiThread(boolean enabled) { - if (mFastScroller != null) { - mFastScroller.setEnabled(enabled); + if (mFastScroll != null) { + mFastScroll.setEnabled(enabled); } else if (enabled) { - mFastScroller = new FastScroller(this); - mFastScroller.setEnabled(true); + mFastScroll = new FastScroller(this, mFastScrollStyle); + mFastScroll.setEnabled(true); } resolvePadding(); - if (mFastScroller != null) { - mFastScroller.updateLayout(); + if (mFastScroll != null) { + mFastScroll.updateLayout(); + } + } + + /** + * Specifies the style of the fast scroller decorations. + * + * @param styleResId style resource containing fast scroller properties + * @see android.R.styleable#FastScroll + */ + public void setFastScrollStyle(int styleResId) { + if (mFastScroll == null) { + mFastScrollStyle = styleResId; + } else { + mFastScroll.setStyle(styleResId); } } @@ -1288,8 +1324,8 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } private void setFastScrollerAlwaysVisibleUiThread(boolean alwaysShow) { - if (mFastScroller != null) { - mFastScroller.setAlwaysShow(alwaysShow); + if (mFastScroll != null) { + mFastScroll.setAlwaysShow(alwaysShow); } } @@ -1307,17 +1343,17 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te * @see #setFastScrollAlwaysVisible(boolean) */ public boolean isFastScrollAlwaysVisible() { - if (mFastScroller == null) { + if (mFastScroll == null) { return mFastScrollEnabled && mFastScrollAlwaysVisible; } else { - return mFastScroller.isEnabled() && mFastScroller.isAlwaysShowEnabled(); + return mFastScroll.isEnabled() && mFastScroll.isAlwaysShowEnabled(); } } @Override public int getVerticalScrollbarWidth() { - if (mFastScroller != null && mFastScroller.isEnabled()) { - return Math.max(super.getVerticalScrollbarWidth(), mFastScroller.getWidth()); + if (mFastScroll != null && mFastScroll.isEnabled()) { + return Math.max(super.getVerticalScrollbarWidth(), mFastScroll.getWidth()); } return super.getVerticalScrollbarWidth(); } @@ -1330,26 +1366,26 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te */ @ViewDebug.ExportedProperty public boolean isFastScrollEnabled() { - if (mFastScroller == null) { + if (mFastScroll == null) { return mFastScrollEnabled; } else { - return mFastScroller.isEnabled(); + return mFastScroll.isEnabled(); } } @Override public void setVerticalScrollbarPosition(int position) { super.setVerticalScrollbarPosition(position); - if (mFastScroller != null) { - mFastScroller.setScrollbarPosition(position); + if (mFastScroll != null) { + mFastScroll.setScrollbarPosition(position); } } @Override public void setScrollBarStyle(int style) { super.setScrollBarStyle(style); - if (mFastScroller != null) { - mFastScroller.setScrollBarStyle(style); + if (mFastScroll != null) { + mFastScroll.setScrollBarStyle(style); } } @@ -1410,8 +1446,8 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te * Notify our scroll listener (if there is one) of a change in scroll state */ void invokeOnItemScrollListener() { - if (mFastScroller != null) { - mFastScroller.onScroll(mFirstPosition, getChildCount(), mItemCount); + if (mFastScroll != null) { + mFastScroll.onScroll(mFirstPosition, getChildCount(), mItemCount); } if (mOnScrollListener != null) { mOnScrollListener.onScroll(this, mFirstPosition, getChildCount(), mItemCount); @@ -1460,6 +1496,21 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } } + int getSelectionModeForAccessibility() { + final int choiceMode = getChoiceMode(); + switch (choiceMode) { + case CHOICE_MODE_NONE: + return CollectionInfo.SELECTION_MODE_NONE; + case CHOICE_MODE_SINGLE: + return CollectionInfo.SELECTION_MODE_SINGLE; + case CHOICE_MODE_MULTIPLE: + case CHOICE_MODE_MULTIPLE_MODAL: + return CollectionInfo.SELECTION_MODE_MULTIPLE; + default: + return CollectionInfo.SELECTION_MODE_NONE; + } + } + @Override public boolean performAccessibilityAction(int action, Bundle arguments) { if (super.performAccessibilityAction(action, arguments)) { @@ -1576,7 +1627,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } private void useDefaultSelector() { - setSelector(getResources().getDrawable( + setSelector(getContext().getDrawable( com.android.internal.R.drawable.list_selector_background)); } @@ -2090,8 +2141,8 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR; // TODO: Move somewhere sane. This doesn't belong in onLayout(). - if (mFastScroller != null) { - mFastScroller.onItemCountChanged(getChildCount(), mItemCount); + if (mFastScroll != null) { + mFastScroll.onItemCountChanged(getChildCount(), mItemCount); } } @@ -2121,6 +2172,25 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te protected void layoutChildren() { } + /** + * @param focusedView view that holds accessibility focus + * @return direct child that contains accessibility focus, or null if no + * child contains accessibility focus + */ + View getAccessibilityFocusedChild(View focusedView) { + ViewParent viewParent = focusedView.getParent(); + while ((viewParent instanceof View) && (viewParent != this)) { + focusedView = (View) viewParent; + viewParent = viewParent.getParent(); + } + + if (!(viewParent instanceof View)) { + return null; + } + + return focusedView; + } + void updateScrollIndicators() { if (mScrollUp != null) { boolean canScrollUp; @@ -2242,6 +2312,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te // If we failed to re-bind the data, scrap the obtained view. if (updatedView != transientView) { + setItemViewLayoutParams(updatedView, position); mRecycler.addScrapView(updatedView, position); } } @@ -2260,12 +2331,6 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } else { isScrap[0] = true; - // Clear any system-managed transient state so that we can - // recycle this view and bind it to different data. - if (child.isAccessibilityFocused()) { - child.clearAccessibilityFocus(); - } - child.dispatchFinishTemporaryDetach(); } } @@ -2278,19 +2343,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); } - if (mAdapterHasStableIds) { - final ViewGroup.LayoutParams vlp = child.getLayoutParams(); - LayoutParams lp; - if (vlp == null) { - lp = (LayoutParams) generateDefaultLayoutParams(); - } else if (!checkLayoutParams(vlp)) { - lp = (LayoutParams) generateLayoutParams(vlp); - } else { - lp = (LayoutParams) vlp; - } - lp.itemId = mAdapter.getItemId(position); - child.setLayoutParams(lp); - } + setItemViewLayoutParams(child, position); if (AccessibilityManager.getInstance(mContext).isEnabled()) { if (mAccessibilityDelegate == null) { @@ -2306,6 +2359,24 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te return child; } + private void setItemViewLayoutParams(View child, int position) { + final ViewGroup.LayoutParams vlp = child.getLayoutParams(); + LayoutParams lp; + if (vlp == null) { + lp = (LayoutParams) generateDefaultLayoutParams(); + } else if (!checkLayoutParams(vlp)) { + lp = (LayoutParams) generateLayoutParams(vlp); + } else { + lp = (LayoutParams) vlp; + } + + if (mAdapterHasStableIds) { + lp.itemId = mAdapter.getItemId(position); + } + lp.viewType = mAdapter.getItemViewType(position); + child.setLayoutParams(lp); + } + class ListItemAccessibilityDelegate extends AccessibilityDelegate { @Override public AccessibilityNodeInfo createAccessibilityNodeInfo(View host) { @@ -2506,8 +2577,8 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te rememberSyncState(); } - if (mFastScroller != null) { - mFastScroller.onSizeChanged(w, h, oldw, oldh); + if (mFastScroll != null) { + mFastScroll.onSizeChanged(w, h, oldw, oldh); } } @@ -2566,7 +2637,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te * @attr ref android.R.styleable#AbsListView_listSelector */ public void setSelector(int resID) { - setSelector(getResources().getDrawable(resID)); + setSelector(getContext().getDrawable(resID)); } public void setSelector(Drawable sel) { @@ -2728,6 +2799,8 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te protected void onDetachedFromWindow() { super.onDetachedFromWindow(); + mIsDetaching = true; + // Dismiss the popup in case onSaveInstanceState() was not invoked dismissPopup(); @@ -2776,6 +2849,8 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te removeCallbacks(mTouchModeReset); mTouchModeReset.run(); } + + mIsDetaching = false; } @Override @@ -2836,8 +2911,8 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te @Override public void onRtlPropertiesChanged(int layoutDirection) { super.onRtlPropertiesChanged(layoutDirection); - if (mFastScroller != null) { - mFastScroller.setScrollbarPosition(getVerticalScrollbarPosition()); + if (mFastScroll != null) { + mFastScroll.setScrollbarPosition(getVerticalScrollbarPosition()); } } @@ -3039,7 +3114,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te @Override public boolean onKeyUp(int keyCode, KeyEvent event) { - if (event.isConfirmKey()) { + if (KeyEvent.isConfirmKey(keyCode)) { if (!isEnabled()) { return true; } @@ -3402,7 +3477,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te mPositionScroller.stop(); } - if (!isAttachedToWindow()) { + if (mIsDetaching || !isAttachedToWindow()) { // Something isn't right. // Since we rely on being attached to get data set change notifications, // don't risk doing anything where we might try to resync and find things @@ -3410,8 +3485,8 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te return false; } - if (mFastScroller != null) { - boolean intercepted = mFastScroller.onTouchEvent(ev); + if (mFastScroll != null) { + boolean intercepted = mFastScroll.onTouchEvent(ev); if (intercepted) { return true; } @@ -3641,7 +3716,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te mTouchMode = TOUCH_MODE_REST; child.setPressed(false); setPressed(false); - if (!mDataChanged && isAttachedToWindow()) { + if (!mDataChanged && !mIsDetaching && isAttachedToWindow()) { performClick.run(); } } @@ -3900,7 +3975,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te @Override public boolean onInterceptHoverEvent(MotionEvent event) { - if (mFastScroller != null && mFastScroller.onInterceptHoverEvent(event)) { + if (mFastScroll != null && mFastScroll.onInterceptHoverEvent(event)) { return true; } @@ -3916,7 +3991,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te mPositionScroller.stop(); } - if (!isAttachedToWindow()) { + if (mIsDetaching || !isAttachedToWindow()) { // Something isn't right. // Since we rely on being attached to get data set change notifications, // don't risk doing anything where we might try to resync and find things @@ -3924,7 +3999,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te return false; } - if (mFastScroller != null && mFastScroller.onInterceptTouchEvent(ev)) { + if (mFastScroll != null && mFastScroll.onInterceptTouchEvent(ev)) { return true; } @@ -4321,447 +4396,6 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } } - class PositionScroller implements Runnable { - private static final int SCROLL_DURATION = 200; - - private static final int MOVE_DOWN_POS = 1; - private static final int MOVE_UP_POS = 2; - private static final int MOVE_DOWN_BOUND = 3; - private static final int MOVE_UP_BOUND = 4; - private static final int MOVE_OFFSET = 5; - - private int mMode; - private int mTargetPos; - private int mBoundPos; - private int mLastSeenPos; - private int mScrollDuration; - private final int mExtraScroll; - - private int mOffsetFromTop; - - PositionScroller() { - mExtraScroll = ViewConfiguration.get(mContext).getScaledFadingEdgeLength(); - } - - void start(final int position) { - stop(); - - if (mDataChanged) { - // Wait until we're back in a stable state to try this. - mPositionScrollAfterLayout = new Runnable() { - @Override public void run() { - start(position); - } - }; - return; - } - - final int childCount = getChildCount(); - if (childCount == 0) { - // Can't scroll without children. - return; - } - - final int firstPos = mFirstPosition; - final int lastPos = firstPos + childCount - 1; - - int viewTravelCount; - int clampedPosition = Math.max(0, Math.min(getCount() - 1, position)); - if (clampedPosition < firstPos) { - viewTravelCount = firstPos - clampedPosition + 1; - mMode = MOVE_UP_POS; - } else if (clampedPosition > lastPos) { - viewTravelCount = clampedPosition - lastPos + 1; - mMode = MOVE_DOWN_POS; - } else { - scrollToVisible(clampedPosition, INVALID_POSITION, SCROLL_DURATION); - return; - } - - if (viewTravelCount > 0) { - mScrollDuration = SCROLL_DURATION / viewTravelCount; - } else { - mScrollDuration = SCROLL_DURATION; - } - mTargetPos = clampedPosition; - mBoundPos = INVALID_POSITION; - mLastSeenPos = INVALID_POSITION; - - postOnAnimation(this); - } - - void start(final int position, final int boundPosition) { - stop(); - - if (boundPosition == INVALID_POSITION) { - start(position); - return; - } - - if (mDataChanged) { - // Wait until we're back in a stable state to try this. - mPositionScrollAfterLayout = new Runnable() { - @Override public void run() { - start(position, boundPosition); - } - }; - return; - } - - final int childCount = getChildCount(); - if (childCount == 0) { - // Can't scroll without children. - return; - } - - final int firstPos = mFirstPosition; - final int lastPos = firstPos + childCount - 1; - - int viewTravelCount; - int clampedPosition = Math.max(0, Math.min(getCount() - 1, position)); - if (clampedPosition < firstPos) { - final int boundPosFromLast = lastPos - boundPosition; - if (boundPosFromLast < 1) { - // Moving would shift our bound position off the screen. Abort. - return; - } - - final int posTravel = firstPos - clampedPosition + 1; - final int boundTravel = boundPosFromLast - 1; - if (boundTravel < posTravel) { - viewTravelCount = boundTravel; - mMode = MOVE_UP_BOUND; - } else { - viewTravelCount = posTravel; - mMode = MOVE_UP_POS; - } - } else if (clampedPosition > lastPos) { - final int boundPosFromFirst = boundPosition - firstPos; - if (boundPosFromFirst < 1) { - // Moving would shift our bound position off the screen. Abort. - return; - } - - final int posTravel = clampedPosition - lastPos + 1; - final int boundTravel = boundPosFromFirst - 1; - if (boundTravel < posTravel) { - viewTravelCount = boundTravel; - mMode = MOVE_DOWN_BOUND; - } else { - viewTravelCount = posTravel; - mMode = MOVE_DOWN_POS; - } - } else { - scrollToVisible(clampedPosition, boundPosition, SCROLL_DURATION); - return; - } - - if (viewTravelCount > 0) { - mScrollDuration = SCROLL_DURATION / viewTravelCount; - } else { - mScrollDuration = SCROLL_DURATION; - } - mTargetPos = clampedPosition; - mBoundPos = boundPosition; - mLastSeenPos = INVALID_POSITION; - - postOnAnimation(this); - } - - void startWithOffset(int position, int offset) { - startWithOffset(position, offset, SCROLL_DURATION); - } - - void startWithOffset(final int position, int offset, final int duration) { - stop(); - - if (mDataChanged) { - // Wait until we're back in a stable state to try this. - final int postOffset = offset; - mPositionScrollAfterLayout = new Runnable() { - @Override public void run() { - startWithOffset(position, postOffset, duration); - } - }; - return; - } - - final int childCount = getChildCount(); - if (childCount == 0) { - // Can't scroll without children. - return; - } - - offset += getPaddingTop(); - - mTargetPos = Math.max(0, Math.min(getCount() - 1, position)); - mOffsetFromTop = offset; - mBoundPos = INVALID_POSITION; - mLastSeenPos = INVALID_POSITION; - mMode = MOVE_OFFSET; - - final int firstPos = mFirstPosition; - final int lastPos = firstPos + childCount - 1; - - int viewTravelCount; - if (mTargetPos < firstPos) { - viewTravelCount = firstPos - mTargetPos; - } else if (mTargetPos > lastPos) { - viewTravelCount = mTargetPos - lastPos; - } else { - // On-screen, just scroll. - final int targetTop = getChildAt(mTargetPos - firstPos).getTop(); - smoothScrollBy(targetTop - offset, duration, true); - return; - } - - // Estimate how many screens we should travel - final float screenTravelCount = (float) viewTravelCount / childCount; - mScrollDuration = screenTravelCount < 1 ? - duration : (int) (duration / screenTravelCount); - mLastSeenPos = INVALID_POSITION; - - postOnAnimation(this); - } - - /** - * Scroll such that targetPos is in the visible padded region without scrolling - * boundPos out of view. Assumes targetPos is onscreen. - */ - void scrollToVisible(int targetPos, int boundPos, int duration) { - final int firstPos = mFirstPosition; - final int childCount = getChildCount(); - final int lastPos = firstPos + childCount - 1; - final int paddedTop = mListPadding.top; - final int paddedBottom = getHeight() - mListPadding.bottom; - - if (targetPos < firstPos || targetPos > lastPos) { - Log.w(TAG, "scrollToVisible called with targetPos " + targetPos + - " not visible [" + firstPos + ", " + lastPos + "]"); - } - if (boundPos < firstPos || boundPos > lastPos) { - // boundPos doesn't matter, it's already offscreen. - boundPos = INVALID_POSITION; - } - - final View targetChild = getChildAt(targetPos - firstPos); - final int targetTop = targetChild.getTop(); - final int targetBottom = targetChild.getBottom(); - int scrollBy = 0; - - if (targetBottom > paddedBottom) { - scrollBy = targetBottom - paddedBottom; - } - if (targetTop < paddedTop) { - scrollBy = targetTop - paddedTop; - } - - if (scrollBy == 0) { - return; - } - - if (boundPos >= 0) { - final View boundChild = getChildAt(boundPos - firstPos); - final int boundTop = boundChild.getTop(); - final int boundBottom = boundChild.getBottom(); - final int absScroll = Math.abs(scrollBy); - - if (scrollBy < 0 && boundBottom + absScroll > paddedBottom) { - // Don't scroll the bound view off the bottom of the screen. - scrollBy = Math.max(0, boundBottom - paddedBottom); - } else if (scrollBy > 0 && boundTop - absScroll < paddedTop) { - // Don't scroll the bound view off the top of the screen. - scrollBy = Math.min(0, boundTop - paddedTop); - } - } - - smoothScrollBy(scrollBy, duration); - } - - void stop() { - removeCallbacks(this); - } - - @Override - public void run() { - final int listHeight = getHeight(); - final int firstPos = mFirstPosition; - - switch (mMode) { - case MOVE_DOWN_POS: { - final int lastViewIndex = getChildCount() - 1; - final int lastPos = firstPos + lastViewIndex; - - if (lastViewIndex < 0) { - return; - } - - if (lastPos == mLastSeenPos) { - // No new views, let things keep going. - postOnAnimation(this); - return; - } - - final View lastView = getChildAt(lastViewIndex); - final int lastViewHeight = lastView.getHeight(); - final int lastViewTop = lastView.getTop(); - final int lastViewPixelsShowing = listHeight - lastViewTop; - final int extraScroll = lastPos < mItemCount - 1 ? - Math.max(mListPadding.bottom, mExtraScroll) : mListPadding.bottom; - - final int scrollBy = lastViewHeight - lastViewPixelsShowing + extraScroll; - smoothScrollBy(scrollBy, mScrollDuration, true); - - mLastSeenPos = lastPos; - if (lastPos < mTargetPos) { - postOnAnimation(this); - } - break; - } - - case MOVE_DOWN_BOUND: { - final int nextViewIndex = 1; - final int childCount = getChildCount(); - - if (firstPos == mBoundPos || childCount <= nextViewIndex - || firstPos + childCount >= mItemCount) { - return; - } - final int nextPos = firstPos + nextViewIndex; - - if (nextPos == mLastSeenPos) { - // No new views, let things keep going. - postOnAnimation(this); - return; - } - - final View nextView = getChildAt(nextViewIndex); - final int nextViewHeight = nextView.getHeight(); - final int nextViewTop = nextView.getTop(); - final int extraScroll = Math.max(mListPadding.bottom, mExtraScroll); - if (nextPos < mBoundPos) { - smoothScrollBy(Math.max(0, nextViewHeight + nextViewTop - extraScroll), - mScrollDuration, true); - - mLastSeenPos = nextPos; - - postOnAnimation(this); - } else { - if (nextViewTop > extraScroll) { - smoothScrollBy(nextViewTop - extraScroll, mScrollDuration, true); - } - } - break; - } - - case MOVE_UP_POS: { - if (firstPos == mLastSeenPos) { - // No new views, let things keep going. - postOnAnimation(this); - return; - } - - final View firstView = getChildAt(0); - if (firstView == null) { - return; - } - final int firstViewTop = firstView.getTop(); - final int extraScroll = firstPos > 0 ? - Math.max(mExtraScroll, mListPadding.top) : mListPadding.top; - - smoothScrollBy(firstViewTop - extraScroll, mScrollDuration, true); - - mLastSeenPos = firstPos; - - if (firstPos > mTargetPos) { - postOnAnimation(this); - } - break; - } - - case MOVE_UP_BOUND: { - final int lastViewIndex = getChildCount() - 2; - if (lastViewIndex < 0) { - return; - } - final int lastPos = firstPos + lastViewIndex; - - if (lastPos == mLastSeenPos) { - // No new views, let things keep going. - postOnAnimation(this); - return; - } - - final View lastView = getChildAt(lastViewIndex); - final int lastViewHeight = lastView.getHeight(); - final int lastViewTop = lastView.getTop(); - final int lastViewPixelsShowing = listHeight - lastViewTop; - final int extraScroll = Math.max(mListPadding.top, mExtraScroll); - mLastSeenPos = lastPos; - if (lastPos > mBoundPos) { - smoothScrollBy(-(lastViewPixelsShowing - extraScroll), mScrollDuration, true); - postOnAnimation(this); - } else { - final int bottom = listHeight - extraScroll; - final int lastViewBottom = lastViewTop + lastViewHeight; - if (bottom > lastViewBottom) { - smoothScrollBy(-(bottom - lastViewBottom), mScrollDuration, true); - } - } - break; - } - - case MOVE_OFFSET: { - if (mLastSeenPos == firstPos) { - // No new views, let things keep going. - postOnAnimation(this); - return; - } - - mLastSeenPos = firstPos; - - final int childCount = getChildCount(); - final int position = mTargetPos; - final int lastPos = firstPos + childCount - 1; - - int viewTravelCount = 0; - if (position < firstPos) { - viewTravelCount = firstPos - position + 1; - } else if (position > lastPos) { - viewTravelCount = position - lastPos; - } - - // Estimate how many screens we should travel - final float screenTravelCount = (float) viewTravelCount / childCount; - - final float modifier = Math.min(Math.abs(screenTravelCount), 1.f); - if (position < firstPos) { - final int distance = (int) (-getHeight() * modifier); - final int duration = (int) (mScrollDuration * modifier); - smoothScrollBy(distance, duration, true); - postOnAnimation(this); - } else if (position > lastPos) { - final int distance = (int) (getHeight() * modifier); - final int duration = (int) (mScrollDuration * modifier); - smoothScrollBy(distance, duration, true); - postOnAnimation(this); - } else { - // On-screen, just scroll. - final int targetTop = getChildAt(position - firstPos).getTop(); - final int distance = targetTop - mOffsetFromTop; - final int duration = (int) (mScrollDuration * - ((float) Math.abs(distance) / getHeight())); - smoothScrollBy(distance, duration, true); - } - break; - } - - default: - break; - } - } - } - /** * The amount of friction applied to flings. The default value * is {@link ViewConfiguration#getScrollFriction}. @@ -4784,20 +4418,27 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } /** + * Override this for better control over position scrolling. + */ + AbsPositionScroller createPositionScroller() { + return new PositionScroller(); + } + + /** * Smoothly scroll to the specified adapter position. The view will * scroll such that the indicated position is displayed. * @param position Scroll to this adapter position. */ public void smoothScrollToPosition(int position) { if (mPositionScroller == null) { - mPositionScroller = new PositionScroller(); + mPositionScroller = createPositionScroller(); } mPositionScroller.start(position); } /** * Smoothly scroll to the specified adapter position. The view will scroll - * such that the indicated position is displayed <code>offset</code> pixels from + * such that the indicated position is displayed <code>offset</code> pixels below * the top edge of the view. If this is impossible, (e.g. the offset would scroll * the first or last item beyond the boundaries of the list) it will get as close * as possible. The scroll will take <code>duration</code> milliseconds to complete. @@ -4809,14 +4450,14 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te */ public void smoothScrollToPositionFromTop(int position, int offset, int duration) { if (mPositionScroller == null) { - mPositionScroller = new PositionScroller(); + mPositionScroller = createPositionScroller(); } mPositionScroller.startWithOffset(position, offset, duration); } /** * Smoothly scroll to the specified adapter position. The view will scroll - * such that the indicated position is displayed <code>offset</code> pixels from + * such that the indicated position is displayed <code>offset</code> pixels below * the top edge of the view. If this is impossible, (e.g. the offset would scroll * the first or last item beyond the boundaries of the list) it will get as close * as possible. @@ -4827,9 +4468,9 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te */ public void smoothScrollToPositionFromTop(int position, int offset) { if (mPositionScroller == null) { - mPositionScroller = new PositionScroller(); + mPositionScroller = createPositionScroller(); } - mPositionScroller.startWithOffset(position, offset); + mPositionScroller.startWithOffset(position, offset, offset); } /** @@ -4837,13 +4478,14 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te * scroll such that the indicated position is displayed, but it will * stop early if scrolling further would scroll boundPosition out of * view. + * * @param position Scroll to this adapter position. * @param boundPosition Do not scroll if it would move this adapter * position out of view. */ public void smoothScrollToPosition(int position, int boundPosition) { if (mPositionScroller == null) { - mPositionScroller = new PositionScroller(); + mPositionScroller = createPositionScroller(); } mPositionScroller.start(position, boundPosition); } @@ -6285,16 +5927,16 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te @Override public void onChanged() { super.onChanged(); - if (mFastScroller != null) { - mFastScroller.onSectionsChanged(); + if (mFastScroll != null) { + mFastScroll.onSectionsChanged(); } } @Override public void onInvalidated() { super.onInvalidated(); - if (mFastScroller != null) { - mFastScroller.onSectionsChanged(); + if (mFastScroll != null) { + mFastScroll.onSectionsChanged(); } } } @@ -6555,18 +6197,12 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te void clear() { if (mViewTypeCount == 1) { final ArrayList<View> scrap = mCurrentScrap; - final int scrapCount = scrap.size(); - for (int i = 0; i < scrapCount; i++) { - removeDetachedView(scrap.remove(scrapCount - 1 - i), false); - } + clearScrap(scrap); } else { final int typeCount = mViewTypeCount; for (int i = 0; i < typeCount; i++) { final ArrayList<View> scrap = mScrapViews[i]; - final int scrapCount = scrap.size(); - for (int j = 0; j < scrapCount; j++) { - removeDetachedView(scrap.remove(scrapCount - 1 - j), false); - } + clearScrap(scrap); } } @@ -6667,7 +6303,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te if (mViewTypeCount == 1) { return retrieveFromScrap(mCurrentScrap, position); } else { - int whichScrap = mAdapter.getItemViewType(position); + final int whichScrap = mAdapter.getItemViewType(position); if (whichScrap >= 0 && whichScrap < mScrapViews.length) { return retrieveFromScrap(mScrapViews[whichScrap], position); } @@ -6739,13 +6375,6 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te mScrapViews[viewType].add(scrap); } - // Clear any system-managed transient state. - if (scrap.isAccessibilityFocused()) { - scrap.clearAccessibilityFocus(); - } - - scrap.setAccessibilityDelegate(null); - if (mRecyclerListener != null) { mRecyclerListener.onMovedToScrapHeap(scrap); } @@ -6819,7 +6448,6 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te lp.scrappedFromPosition = mFirstActivePosition + i; scrapViews.add(victim); - victim.setAccessibilityDelegate(null); if (hasListener) { mRecyclerListener.onMovedToScrapHeap(victim); } @@ -6923,23 +6551,861 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } } } - } - static View retrieveFromScrap(ArrayList<View> scrapViews, int position) { - int size = scrapViews.size(); - if (size > 0) { - // See if we still have a view for this position. - for (int i=0; i<size; i++) { - View view = scrapViews.get(i); - if (((AbsListView.LayoutParams)view.getLayoutParams()) - .scrappedFromPosition == position) { - scrapViews.remove(i); - return view; + private View retrieveFromScrap(ArrayList<View> scrapViews, int position) { + final int size = scrapViews.size(); + if (size > 0) { + // See if we still have a view for this position or ID. + for (int i = 0; i < size; i++) { + final View view = scrapViews.get(i); + final AbsListView.LayoutParams params = + (AbsListView.LayoutParams) view.getLayoutParams(); + + if (mAdapterHasStableIds) { + final long id = mAdapter.getItemId(position); + if (id == params.itemId) { + return scrapViews.remove(i); + } + } else if (params.scrappedFromPosition == position) { + final View scrap = scrapViews.remove(i); + clearAccessibilityFromScrap(scrap); + return scrap; + } } + final View scrap = scrapViews.remove(size - 1); + clearAccessibilityFromScrap(scrap); + return scrap; + } else { + return null; } - return scrapViews.remove(size - 1); + } + + private void clearScrap(final ArrayList<View> scrap) { + final int scrapCount = scrap.size(); + for (int j = 0; j < scrapCount; j++) { + removeDetachedView(scrap.remove(scrapCount - 1 - j), false); + } + } + + private void clearAccessibilityFromScrap(View view) { + if (view.isAccessibilityFocused()) { + view.clearAccessibilityFocus(); + } + view.setAccessibilityDelegate(null); + } + + private void removeDetachedView(View child, boolean animate) { + child.setAccessibilityDelegate(null); + AbsListView.this.removeDetachedView(child, animate); + } + } + + /** + * Returns the height of the view for the specified position. + * + * @param position the item position + * @return view height in pixels + */ + int getHeightForPosition(int position) { + final int firstVisiblePosition = getFirstVisiblePosition(); + final int childCount = getChildCount(); + final int index = position - firstVisiblePosition; + if (index >= 0 && index < childCount) { + // Position is on-screen, use existing view. + final View view = getChildAt(index); + return view.getHeight(); } else { - return null; + // Position is off-screen, obtain & recycle view. + final View view = obtainView(position, mIsScrap); + view.measure(mWidthMeasureSpec, MeasureSpec.UNSPECIFIED); + final int height = view.getMeasuredHeight(); + mRecycler.addScrapView(view, position); + return height; + } + } + + /** + * Sets the selected item and positions the selection y pixels from the top edge + * of the ListView. (If in touch mode, the item will not be selected but it will + * still be positioned appropriately.) + * + * @param position Index (starting at 0) of the data item to be selected. + * @param y The distance from the top edge of the ListView (plus padding) that the + * item will be positioned. + */ + public void setSelectionFromTop(int position, int y) { + if (mAdapter == null) { + return; + } + + if (!isInTouchMode()) { + position = lookForSelectablePosition(position, true); + if (position >= 0) { + setNextSelectedPositionInt(position); + } + } else { + mResurrectToPosition = position; + } + + if (position >= 0) { + mLayoutMode = LAYOUT_SPECIFIC; + mSpecificTop = mListPadding.top + y; + + if (mNeedSync) { + mSyncPosition = position; + mSyncRowId = mAdapter.getItemId(position); + } + + if (mPositionScroller != null) { + mPositionScroller.stop(); + } + requestLayout(); + } + } + + /** + * Abstract positon scroller used to handle smooth scrolling. + */ + static abstract class AbsPositionScroller { + public abstract void start(int position); + public abstract void start(int position, int boundPosition); + public abstract void startWithOffset(int position, int offset); + public abstract void startWithOffset(int position, int offset, int duration); + public abstract void stop(); + } + + /** + * Default position scroller that simulates a fling. + */ + class PositionScroller extends AbsPositionScroller implements Runnable { + private static final int SCROLL_DURATION = 200; + + private static final int MOVE_DOWN_POS = 1; + private static final int MOVE_UP_POS = 2; + private static final int MOVE_DOWN_BOUND = 3; + private static final int MOVE_UP_BOUND = 4; + private static final int MOVE_OFFSET = 5; + + private int mMode; + private int mTargetPos; + private int mBoundPos; + private int mLastSeenPos; + private int mScrollDuration; + private final int mExtraScroll; + + private int mOffsetFromTop; + + PositionScroller() { + mExtraScroll = ViewConfiguration.get(mContext).getScaledFadingEdgeLength(); + } + + @Override + public void start(final int position) { + stop(); + + if (mDataChanged) { + // Wait until we're back in a stable state to try this. + mPositionScrollAfterLayout = new Runnable() { + @Override public void run() { + start(position); + } + }; + return; + } + + final int childCount = getChildCount(); + if (childCount == 0) { + // Can't scroll without children. + return; + } + + final int firstPos = mFirstPosition; + final int lastPos = firstPos + childCount - 1; + + int viewTravelCount; + int clampedPosition = Math.max(0, Math.min(getCount() - 1, position)); + if (clampedPosition < firstPos) { + viewTravelCount = firstPos - clampedPosition + 1; + mMode = MOVE_UP_POS; + } else if (clampedPosition > lastPos) { + viewTravelCount = clampedPosition - lastPos + 1; + mMode = MOVE_DOWN_POS; + } else { + scrollToVisible(clampedPosition, INVALID_POSITION, SCROLL_DURATION); + return; + } + + if (viewTravelCount > 0) { + mScrollDuration = SCROLL_DURATION / viewTravelCount; + } else { + mScrollDuration = SCROLL_DURATION; + } + mTargetPos = clampedPosition; + mBoundPos = INVALID_POSITION; + mLastSeenPos = INVALID_POSITION; + + postOnAnimation(this); + } + + @Override + public void start(final int position, final int boundPosition) { + stop(); + + if (boundPosition == INVALID_POSITION) { + start(position); + return; + } + + if (mDataChanged) { + // Wait until we're back in a stable state to try this. + mPositionScrollAfterLayout = new Runnable() { + @Override public void run() { + start(position, boundPosition); + } + }; + return; + } + + final int childCount = getChildCount(); + if (childCount == 0) { + // Can't scroll without children. + return; + } + + final int firstPos = mFirstPosition; + final int lastPos = firstPos + childCount - 1; + + int viewTravelCount; + int clampedPosition = Math.max(0, Math.min(getCount() - 1, position)); + if (clampedPosition < firstPos) { + final int boundPosFromLast = lastPos - boundPosition; + if (boundPosFromLast < 1) { + // Moving would shift our bound position off the screen. Abort. + return; + } + + final int posTravel = firstPos - clampedPosition + 1; + final int boundTravel = boundPosFromLast - 1; + if (boundTravel < posTravel) { + viewTravelCount = boundTravel; + mMode = MOVE_UP_BOUND; + } else { + viewTravelCount = posTravel; + mMode = MOVE_UP_POS; + } + } else if (clampedPosition > lastPos) { + final int boundPosFromFirst = boundPosition - firstPos; + if (boundPosFromFirst < 1) { + // Moving would shift our bound position off the screen. Abort. + return; + } + + final int posTravel = clampedPosition - lastPos + 1; + final int boundTravel = boundPosFromFirst - 1; + if (boundTravel < posTravel) { + viewTravelCount = boundTravel; + mMode = MOVE_DOWN_BOUND; + } else { + viewTravelCount = posTravel; + mMode = MOVE_DOWN_POS; + } + } else { + scrollToVisible(clampedPosition, boundPosition, SCROLL_DURATION); + return; + } + + if (viewTravelCount > 0) { + mScrollDuration = SCROLL_DURATION / viewTravelCount; + } else { + mScrollDuration = SCROLL_DURATION; + } + mTargetPos = clampedPosition; + mBoundPos = boundPosition; + mLastSeenPos = INVALID_POSITION; + + postOnAnimation(this); + } + + @Override + public void startWithOffset(int position, int offset) { + startWithOffset(position, offset, SCROLL_DURATION); + } + + @Override + public void startWithOffset(final int position, int offset, final int duration) { + stop(); + + if (mDataChanged) { + // Wait until we're back in a stable state to try this. + final int postOffset = offset; + mPositionScrollAfterLayout = new Runnable() { + @Override public void run() { + startWithOffset(position, postOffset, duration); + } + }; + return; + } + + final int childCount = getChildCount(); + if (childCount == 0) { + // Can't scroll without children. + return; + } + + offset += getPaddingTop(); + + mTargetPos = Math.max(0, Math.min(getCount() - 1, position)); + mOffsetFromTop = offset; + mBoundPos = INVALID_POSITION; + mLastSeenPos = INVALID_POSITION; + mMode = MOVE_OFFSET; + + final int firstPos = mFirstPosition; + final int lastPos = firstPos + childCount - 1; + + int viewTravelCount; + if (mTargetPos < firstPos) { + viewTravelCount = firstPos - mTargetPos; + } else if (mTargetPos > lastPos) { + viewTravelCount = mTargetPos - lastPos; + } else { + // On-screen, just scroll. + final int targetTop = getChildAt(mTargetPos - firstPos).getTop(); + smoothScrollBy(targetTop - offset, duration, true); + return; + } + + // Estimate how many screens we should travel + final float screenTravelCount = (float) viewTravelCount / childCount; + mScrollDuration = screenTravelCount < 1 ? + duration : (int) (duration / screenTravelCount); + mLastSeenPos = INVALID_POSITION; + + postOnAnimation(this); + } + + /** + * Scroll such that targetPos is in the visible padded region without scrolling + * boundPos out of view. Assumes targetPos is onscreen. + */ + private void scrollToVisible(int targetPos, int boundPos, int duration) { + final int firstPos = mFirstPosition; + final int childCount = getChildCount(); + final int lastPos = firstPos + childCount - 1; + final int paddedTop = mListPadding.top; + final int paddedBottom = getHeight() - mListPadding.bottom; + + if (targetPos < firstPos || targetPos > lastPos) { + Log.w(TAG, "scrollToVisible called with targetPos " + targetPos + + " not visible [" + firstPos + ", " + lastPos + "]"); + } + if (boundPos < firstPos || boundPos > lastPos) { + // boundPos doesn't matter, it's already offscreen. + boundPos = INVALID_POSITION; + } + + final View targetChild = getChildAt(targetPos - firstPos); + final int targetTop = targetChild.getTop(); + final int targetBottom = targetChild.getBottom(); + int scrollBy = 0; + + if (targetBottom > paddedBottom) { + scrollBy = targetBottom - paddedBottom; + } + if (targetTop < paddedTop) { + scrollBy = targetTop - paddedTop; + } + + if (scrollBy == 0) { + return; + } + + if (boundPos >= 0) { + final View boundChild = getChildAt(boundPos - firstPos); + final int boundTop = boundChild.getTop(); + final int boundBottom = boundChild.getBottom(); + final int absScroll = Math.abs(scrollBy); + + if (scrollBy < 0 && boundBottom + absScroll > paddedBottom) { + // Don't scroll the bound view off the bottom of the screen. + scrollBy = Math.max(0, boundBottom - paddedBottom); + } else if (scrollBy > 0 && boundTop - absScroll < paddedTop) { + // Don't scroll the bound view off the top of the screen. + scrollBy = Math.min(0, boundTop - paddedTop); + } + } + + smoothScrollBy(scrollBy, duration); + } + + @Override + public void stop() { + removeCallbacks(this); + } + + @Override + public void run() { + final int listHeight = getHeight(); + final int firstPos = mFirstPosition; + + switch (mMode) { + case MOVE_DOWN_POS: { + final int lastViewIndex = getChildCount() - 1; + final int lastPos = firstPos + lastViewIndex; + + if (lastViewIndex < 0) { + return; + } + + if (lastPos == mLastSeenPos) { + // No new views, let things keep going. + postOnAnimation(this); + return; + } + + final View lastView = getChildAt(lastViewIndex); + final int lastViewHeight = lastView.getHeight(); + final int lastViewTop = lastView.getTop(); + final int lastViewPixelsShowing = listHeight - lastViewTop; + final int extraScroll = lastPos < mItemCount - 1 ? + Math.max(mListPadding.bottom, mExtraScroll) : mListPadding.bottom; + + final int scrollBy = lastViewHeight - lastViewPixelsShowing + extraScroll; + smoothScrollBy(scrollBy, mScrollDuration, true); + + mLastSeenPos = lastPos; + if (lastPos < mTargetPos) { + postOnAnimation(this); + } + break; + } + + case MOVE_DOWN_BOUND: { + final int nextViewIndex = 1; + final int childCount = getChildCount(); + + if (firstPos == mBoundPos || childCount <= nextViewIndex + || firstPos + childCount >= mItemCount) { + return; + } + final int nextPos = firstPos + nextViewIndex; + + if (nextPos == mLastSeenPos) { + // No new views, let things keep going. + postOnAnimation(this); + return; + } + + final View nextView = getChildAt(nextViewIndex); + final int nextViewHeight = nextView.getHeight(); + final int nextViewTop = nextView.getTop(); + final int extraScroll = Math.max(mListPadding.bottom, mExtraScroll); + if (nextPos < mBoundPos) { + smoothScrollBy(Math.max(0, nextViewHeight + nextViewTop - extraScroll), + mScrollDuration, true); + + mLastSeenPos = nextPos; + + postOnAnimation(this); + } else { + if (nextViewTop > extraScroll) { + smoothScrollBy(nextViewTop - extraScroll, mScrollDuration, true); + } + } + break; + } + + case MOVE_UP_POS: { + if (firstPos == mLastSeenPos) { + // No new views, let things keep going. + postOnAnimation(this); + return; + } + + final View firstView = getChildAt(0); + if (firstView == null) { + return; + } + final int firstViewTop = firstView.getTop(); + final int extraScroll = firstPos > 0 ? + Math.max(mExtraScroll, mListPadding.top) : mListPadding.top; + + smoothScrollBy(firstViewTop - extraScroll, mScrollDuration, true); + + mLastSeenPos = firstPos; + + if (firstPos > mTargetPos) { + postOnAnimation(this); + } + break; + } + + case MOVE_UP_BOUND: { + final int lastViewIndex = getChildCount() - 2; + if (lastViewIndex < 0) { + return; + } + final int lastPos = firstPos + lastViewIndex; + + if (lastPos == mLastSeenPos) { + // No new views, let things keep going. + postOnAnimation(this); + return; + } + + final View lastView = getChildAt(lastViewIndex); + final int lastViewHeight = lastView.getHeight(); + final int lastViewTop = lastView.getTop(); + final int lastViewPixelsShowing = listHeight - lastViewTop; + final int extraScroll = Math.max(mListPadding.top, mExtraScroll); + mLastSeenPos = lastPos; + if (lastPos > mBoundPos) { + smoothScrollBy(-(lastViewPixelsShowing - extraScroll), mScrollDuration, true); + postOnAnimation(this); + } else { + final int bottom = listHeight - extraScroll; + final int lastViewBottom = lastViewTop + lastViewHeight; + if (bottom > lastViewBottom) { + smoothScrollBy(-(bottom - lastViewBottom), mScrollDuration, true); + } + } + break; + } + + case MOVE_OFFSET: { + if (mLastSeenPos == firstPos) { + // No new views, let things keep going. + postOnAnimation(this); + return; + } + + mLastSeenPos = firstPos; + + final int childCount = getChildCount(); + final int position = mTargetPos; + final int lastPos = firstPos + childCount - 1; + + int viewTravelCount = 0; + if (position < firstPos) { + viewTravelCount = firstPos - position + 1; + } else if (position > lastPos) { + viewTravelCount = position - lastPos; + } + + // Estimate how many screens we should travel + final float screenTravelCount = (float) viewTravelCount / childCount; + + final float modifier = Math.min(Math.abs(screenTravelCount), 1.f); + if (position < firstPos) { + final int distance = (int) (-getHeight() * modifier); + final int duration = (int) (mScrollDuration * modifier); + smoothScrollBy(distance, duration, true); + postOnAnimation(this); + } else if (position > lastPos) { + final int distance = (int) (getHeight() * modifier); + final int duration = (int) (mScrollDuration * modifier); + smoothScrollBy(distance, duration, true); + postOnAnimation(this); + } else { + // On-screen, just scroll. + final int targetTop = getChildAt(position - firstPos).getTop(); + final int distance = targetTop - mOffsetFromTop; + final int duration = (int) (mScrollDuration * + ((float) Math.abs(distance) / getHeight())); + smoothScrollBy(distance, duration, true); + } + break; + } + + default: + break; + } + } + } + + /** + * Abstract position scroller that handles sub-position scrolling but has no + * understanding of layout. + */ + abstract class AbsSubPositionScroller extends AbsPositionScroller { + private static final int DURATION_AUTO = -1; + + private static final int DURATION_AUTO_MIN = 100; + private static final int DURATION_AUTO_MAX = 500; + + private final SubScroller mSubScroller = new SubScroller(); + + /** + * The target offset in pixels between the top of the list and the top + * of the target position. + */ + private int mOffset; + + /** + * Scroll the minimum amount to get the target view entirely on-screen. + */ + private void scrollToPosition(final int targetPosition, final boolean useOffset, + final int offset, final int boundPosition, final int duration) { + stop(); + + if (mDataChanged) { + // Wait until we're back in a stable state to try this. + mPositionScrollAfterLayout = new Runnable() { + @Override + public void run() { + scrollToPosition( + targetPosition, useOffset, offset, boundPosition, duration); + } + }; + return; + } + + if (mAdapter == null) { + // Can't scroll anywhere without an adapter. + return; + } + + final int itemCount = getCount(); + final int clampedPosition = MathUtils.constrain(targetPosition, 0, itemCount - 1); + final int clampedBoundPosition = MathUtils.constrain(boundPosition, -1, itemCount - 1); + final int firstPosition = getFirstVisiblePosition(); + final int lastPosition = firstPosition + getChildCount(); + final int targetRow = getRowForPosition(clampedPosition); + final int firstRow = getRowForPosition(firstPosition); + final int lastRow = getRowForPosition(lastPosition); + if (useOffset || targetRow <= firstRow) { + // Offset so the target row is top-aligned. + mOffset = offset; + } else if (targetRow >= lastRow - 1) { + // Offset so the target row is bottom-aligned. + final int listHeight = getHeight() - getPaddingTop() - getPaddingBottom(); + mOffset = getHeightForPosition(clampedPosition) - listHeight; + } else { + // Don't scroll, target is entirely on-screen. + return; + } + + float endSubRow = targetRow; + if (clampedBoundPosition != INVALID_POSITION) { + final int boundRow = getRowForPosition(clampedBoundPosition); + if (boundRow >= firstRow && boundRow < lastRow && boundRow != targetRow) { + endSubRow = computeBoundSubRow(targetRow, boundRow); + } + } + + final View firstChild = getChildAt(0); + if (firstChild == null) { + return; + } + + final int firstChildHeight = firstChild.getHeight(); + final float startOffsetRatio; + if (firstChildHeight == 0) { + startOffsetRatio = 0; + } else { + startOffsetRatio = -firstChild.getTop() / (float) firstChildHeight; + } + + final float startSubRow = MathUtils.constrain( + firstRow + startOffsetRatio, 0, getCount()); + if (startSubRow == endSubRow && mOffset == 0) { + // Don't scroll, target is already in position. + return; + } + + final int durationMillis; + if (duration == DURATION_AUTO) { + final float subRowDelta = Math.abs(startSubRow - endSubRow); + durationMillis = (int) MathUtils.lerp( + DURATION_AUTO_MIN, DURATION_AUTO_MAX, subRowDelta / getCount()); + } else { + durationMillis = duration; + } + + mSubScroller.startScroll(startSubRow, endSubRow, durationMillis); + + postOnAnimation(mAnimationFrame); + } + + /** + * Given a target row and offset, computes the sub-row position that + * aligns with the top of the list. If the offset is negative, the + * resulting sub-row will be smaller than the target row. + */ + private float resolveOffset(int targetRow, int offset) { + // Compute the target sub-row position by finding the actual row + // indicated by the target and offset. + int remainingOffset = offset; + int targetHeight = getHeightForRow(targetRow); + if (offset < 0) { + // Subtract row heights until we find the right row. + while (targetRow > 0 && remainingOffset < 0) { + remainingOffset += targetHeight; + targetRow--; + targetHeight = getHeightForRow(targetRow); + } + } else if (offset > 0) { + // Add row heights until we find the right row. + while (targetRow < getCount() - 1 && remainingOffset > targetHeight) { + remainingOffset -= targetHeight; + targetRow++; + targetHeight = getHeightForRow(targetRow); + } + } + + final float targetOffsetRatio; + if (remainingOffset < 0 || targetHeight == 0) { + targetOffsetRatio = 0; + } else { + targetOffsetRatio = remainingOffset / (float) targetHeight; + } + + return targetRow + targetOffsetRatio; + } + + private float computeBoundSubRow(int targetRow, int boundRow) { + final float targetSubRow = resolveOffset(targetRow, mOffset); + mOffset = 0; + + // The target row is below the bound row, so the end position would + // push the bound position above the list. Abort! + if (targetSubRow >= boundRow) { + return boundRow; + } + + // Compute the closest possible sub-position that wouldn't push the + // bound position's view further below the list. + final int listHeight = getHeight() - getPaddingTop() - getPaddingBottom(); + final int boundHeight = getHeightForRow(boundRow); + final float boundSubRow = resolveOffset(boundRow, -listHeight + boundHeight); + + return Math.max(boundSubRow, targetSubRow); + } + + @Override + public void start(int position) { + scrollToPosition(position, false, 0, INVALID_POSITION, DURATION_AUTO); + } + + @Override + public void start(int position, int boundPosition) { + scrollToPosition(position, false, 0, boundPosition, DURATION_AUTO); + } + + @Override + public void startWithOffset(int position, int offset) { + scrollToPosition(position, true, offset, INVALID_POSITION, DURATION_AUTO); + } + + @Override + public void startWithOffset(int position, int offset, int duration) { + scrollToPosition(position, true, offset, INVALID_POSITION, duration); + } + + @Override + public void stop() { + removeCallbacks(mAnimationFrame); + } + + /** + * Returns the height of a row, which is computed as the maximum height of + * the items in the row. + * + * @param row the row index + * @return row height in pixels + */ + public abstract int getHeightForRow(int row); + + /** + * Returns the row for the specified item position. + * + * @param position the item position + * @return the row index + */ + public abstract int getRowForPosition(int position); + + /** + * Returns the first item position within the specified row. + * + * @param row the row + * @return the position of the first item in the row + */ + public abstract int getFirstPositionForRow(int row); + + private void onAnimationFrame() { + final boolean shouldPost = mSubScroller.computePosition(); + final float subRow = mSubScroller.getPosition(); + + final int row = (int) subRow; + final int position = getFirstPositionForRow(row); + if (position >= getCount()) { + // Invalid position, abort scrolling. + return; + } + + final int rowHeight = getHeightForRow(row); + final int offset = (int) (rowHeight * (subRow - row)); + final int addOffset = (int) (mOffset * mSubScroller.getInterpolatedValue()); + setSelectionFromTop(position, -offset - addOffset); + + if (shouldPost) { + postOnAnimation(mAnimationFrame); + } + } + + private Runnable mAnimationFrame = new Runnable() { + @Override + public void run() { + onAnimationFrame(); + } + }; + } + + /** + * Scroller capable of returning floating point positions. + */ + static class SubScroller { + private static final Interpolator INTERPOLATOR = new AccelerateDecelerateInterpolator(); + + private float mStartPosition; + private float mEndPosition; + private long mStartTime; + private long mDuration; + + private float mPosition; + private float mInterpolatedValue; + + public void startScroll(float startPosition, float endPosition, int duration) { + mStartPosition = startPosition; + mEndPosition = endPosition; + mDuration = duration; + + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mPosition = startPosition; + mInterpolatedValue = 0; + } + + public boolean computePosition() { + final long elapsed = AnimationUtils.currentAnimationTimeMillis() - mStartTime; + final float value; + if (mDuration <= 0) { + value = 1; + } else { + value = MathUtils.constrain(elapsed / (float) mDuration, 0, 1); + } + + mInterpolatedValue = INTERPOLATOR.getInterpolation(value); + mPosition = (mEndPosition - mStartPosition) * mInterpolatedValue + mStartPosition; + + return elapsed < mDuration; + } + + public float getPosition() { + return mPosition; + } + + public float getInterpolatedValue() { + return mInterpolatedValue; } } } diff --git a/core/java/android/widget/AbsSeekBar.java b/core/java/android/widget/AbsSeekBar.java index fe2fc96..438a9da 100644 --- a/core/java/android/widget/AbsSeekBar.java +++ b/core/java/android/widget/AbsSeekBar.java @@ -65,11 +65,15 @@ public abstract class AbsSeekBar extends ProgressBar { super(context, attrs); } - public AbsSeekBar(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public AbsSeekBar(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public AbsSeekBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.SeekBar, defStyle, 0); + TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.SeekBar, defStyleAttr, defStyleRes); Drawable thumb = a.getDrawable(com.android.internal.R.styleable.SeekBar_thumb); setThumb(thumb); // will guess mThumbOffset if thumb != null... // ...but allow layout to override this diff --git a/core/java/android/widget/AbsSpinner.java b/core/java/android/widget/AbsSpinner.java index f26527f..6a4ad75 100644 --- a/core/java/android/widget/AbsSpinner.java +++ b/core/java/android/widget/AbsSpinner.java @@ -64,12 +64,16 @@ public abstract class AbsSpinner extends AdapterView<SpinnerAdapter> { this(context, attrs, 0); } - public AbsSpinner(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public AbsSpinner(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public AbsSpinner(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); initAbsSpinner(); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.AbsSpinner, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.AbsSpinner, defStyleAttr, defStyleRes); CharSequence[] entries = a.getTextArray(R.styleable.AbsSpinner_entries); if (entries != null) { diff --git a/core/java/android/widget/AbsoluteLayout.java b/core/java/android/widget/AbsoluteLayout.java index 7df6aab..4ce0d5d 100644 --- a/core/java/android/widget/AbsoluteLayout.java +++ b/core/java/android/widget/AbsoluteLayout.java @@ -40,16 +40,19 @@ import android.widget.RemoteViews.RemoteView; @RemoteView public class AbsoluteLayout extends ViewGroup { public AbsoluteLayout(Context context) { - super(context); + this(context, null); } public AbsoluteLayout(Context context, AttributeSet attrs) { - super(context, attrs); + this(context, attrs, 0); } - public AbsoluteLayout(Context context, AttributeSet attrs, - int defStyle) { - super(context, attrs, defStyle); + public AbsoluteLayout(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public AbsoluteLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); } @Override diff --git a/core/java/android/widget/ActionMenuPresenter.java b/core/java/android/widget/ActionMenuPresenter.java new file mode 100644 index 0000000..e4575e5 --- /dev/null +++ b/core/java/android/widget/ActionMenuPresenter.java @@ -0,0 +1,746 @@ +/* + * Copyright (C) 2011 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.widget; + +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.SparseBooleanArray; +import android.view.ActionProvider; +import android.view.Gravity; +import android.view.MenuItem; +import android.view.SoundEffectConstants; +import android.view.View; +import android.view.View.MeasureSpec; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.ListPopupWindow.ForwardingListener; +import com.android.internal.transition.ActionBarTransition; +import com.android.internal.view.ActionBarPolicy; +import com.android.internal.view.menu.ActionMenuItemView; +import com.android.internal.view.menu.BaseMenuPresenter; +import com.android.internal.view.menu.MenuBuilder; +import com.android.internal.view.menu.MenuItemImpl; +import com.android.internal.view.menu.MenuPopupHelper; +import com.android.internal.view.menu.MenuView; +import com.android.internal.view.menu.SubMenuBuilder; + +import java.util.ArrayList; + +/** + * MenuPresenter for building action menus as seen in the action bar and action modes. + * + * @hide + */ +public class ActionMenuPresenter extends BaseMenuPresenter + implements ActionProvider.SubUiVisibilityListener { + private static final String TAG = "ActionMenuPresenter"; + + private View mOverflowButton; + private boolean mReserveOverflow; + private boolean mReserveOverflowSet; + private int mWidthLimit; + private int mActionItemWidthLimit; + private int mMaxItems; + private boolean mMaxItemsSet; + private boolean mStrictWidthLimit; + private boolean mWidthLimitSet; + private boolean mExpandedActionViewsExclusive; + + private int mMinCellSize; + + // Group IDs that have been added as actions - used temporarily, allocated here for reuse. + private final SparseBooleanArray mActionButtonGroups = new SparseBooleanArray(); + + private View mScrapActionButtonView; + + private OverflowPopup mOverflowPopup; + private ActionButtonSubmenu mActionButtonPopup; + + private OpenOverflowRunnable mPostedOpenRunnable; + private ActionMenuPopupCallback mPopupCallback; + + final PopupPresenterCallback mPopupPresenterCallback = new PopupPresenterCallback(); + int mOpenSubMenuId; + + public ActionMenuPresenter(Context context) { + super(context, com.android.internal.R.layout.action_menu_layout, + com.android.internal.R.layout.action_menu_item_layout); + } + + @Override + public void initForMenu(Context context, MenuBuilder menu) { + super.initForMenu(context, menu); + + final Resources res = context.getResources(); + + final ActionBarPolicy abp = ActionBarPolicy.get(context); + if (!mReserveOverflowSet) { + mReserveOverflow = abp.showsOverflowMenuButton(); + } + + if (!mWidthLimitSet) { + mWidthLimit = abp.getEmbeddedMenuWidthLimit(); + } + + // Measure for initial configuration + if (!mMaxItemsSet) { + mMaxItems = abp.getMaxActionButtons(); + } + + int width = mWidthLimit; + if (mReserveOverflow) { + if (mOverflowButton == null) { + mOverflowButton = new OverflowMenuButton(mSystemContext); + final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + mOverflowButton.measure(spec, spec); + } + width -= mOverflowButton.getMeasuredWidth(); + } else { + mOverflowButton = null; + } + + mActionItemWidthLimit = width; + + mMinCellSize = (int) (ActionMenuView.MIN_CELL_SIZE * res.getDisplayMetrics().density); + + // Drop a scrap view as it may no longer reflect the proper context/config. + mScrapActionButtonView = null; + } + + public void onConfigurationChanged(Configuration newConfig) { + if (!mMaxItemsSet) { + mMaxItems = mContext.getResources().getInteger( + com.android.internal.R.integer.max_action_buttons); + } + if (mMenu != null) { + mMenu.onItemsChanged(true); + } + } + + public void setWidthLimit(int width, boolean strict) { + mWidthLimit = width; + mStrictWidthLimit = strict; + mWidthLimitSet = true; + } + + public void setReserveOverflow(boolean reserveOverflow) { + mReserveOverflow = reserveOverflow; + mReserveOverflowSet = true; + } + + public void setItemLimit(int itemCount) { + mMaxItems = itemCount; + mMaxItemsSet = true; + } + + public void setExpandedActionViewsExclusive(boolean isExclusive) { + mExpandedActionViewsExclusive = isExclusive; + } + + @Override + public MenuView getMenuView(ViewGroup root) { + MenuView result = super.getMenuView(root); + ((ActionMenuView) result).setPresenter(this); + return result; + } + + @Override + public View getItemView(final MenuItemImpl item, View convertView, ViewGroup parent) { + View actionView = item.getActionView(); + if (actionView == null || item.hasCollapsibleActionView()) { + actionView = super.getItemView(item, convertView, parent); + } + actionView.setVisibility(item.isActionViewExpanded() ? View.GONE : View.VISIBLE); + + final ActionMenuView menuParent = (ActionMenuView) parent; + final ViewGroup.LayoutParams lp = actionView.getLayoutParams(); + if (!menuParent.checkLayoutParams(lp)) { + actionView.setLayoutParams(menuParent.generateLayoutParams(lp)); + } + return actionView; + } + + @Override + public void bindItemView(MenuItemImpl item, MenuView.ItemView itemView) { + itemView.initialize(item, 0); + + final ActionMenuView menuView = (ActionMenuView) mMenuView; + final ActionMenuItemView actionItemView = (ActionMenuItemView) itemView; + actionItemView.setItemInvoker(menuView); + + if (mPopupCallback == null) { + mPopupCallback = new ActionMenuPopupCallback(); + } + actionItemView.setPopupCallback(mPopupCallback); + } + + @Override + public boolean shouldIncludeItem(int childIndex, MenuItemImpl item) { + return item.isActionButton(); + } + + @Override + public void updateMenuView(boolean cleared) { + final ViewGroup menuViewParent = (ViewGroup) ((View) mMenuView).getParent(); + if (menuViewParent != null) { + ActionBarTransition.beginDelayedTransition(menuViewParent); + } + super.updateMenuView(cleared); + + ((View) mMenuView).requestLayout(); + + if (mMenu != null) { + final ArrayList<MenuItemImpl> actionItems = mMenu.getActionItems(); + final int count = actionItems.size(); + for (int i = 0; i < count; i++) { + final ActionProvider provider = actionItems.get(i).getActionProvider(); + if (provider != null) { + provider.setSubUiVisibilityListener(this); + } + } + } + + final ArrayList<MenuItemImpl> nonActionItems = mMenu != null ? + mMenu.getNonActionItems() : null; + + boolean hasOverflow = false; + if (mReserveOverflow && nonActionItems != null) { + final int count = nonActionItems.size(); + if (count == 1) { + hasOverflow = !nonActionItems.get(0).isActionViewExpanded(); + } else { + hasOverflow = count > 0; + } + } + + if (hasOverflow) { + if (mOverflowButton == null) { + mOverflowButton = new OverflowMenuButton(mSystemContext); + } + ViewGroup parent = (ViewGroup) mOverflowButton.getParent(); + if (parent != mMenuView) { + if (parent != null) { + parent.removeView(mOverflowButton); + } + ActionMenuView menuView = (ActionMenuView) mMenuView; + menuView.addView(mOverflowButton, menuView.generateOverflowButtonLayoutParams()); + } + } else if (mOverflowButton != null && mOverflowButton.getParent() == mMenuView) { + ((ViewGroup) mMenuView).removeView(mOverflowButton); + } + + ((ActionMenuView) mMenuView).setOverflowReserved(mReserveOverflow); + } + + @Override + public boolean filterLeftoverView(ViewGroup parent, int childIndex) { + if (parent.getChildAt(childIndex) == mOverflowButton) return false; + return super.filterLeftoverView(parent, childIndex); + } + + public boolean onSubMenuSelected(SubMenuBuilder subMenu) { + if (!subMenu.hasVisibleItems()) return false; + + SubMenuBuilder topSubMenu = subMenu; + while (topSubMenu.getParentMenu() != mMenu) { + topSubMenu = (SubMenuBuilder) topSubMenu.getParentMenu(); + } + View anchor = findViewForItem(topSubMenu.getItem()); + if (anchor == null) { + if (mOverflowButton == null) return false; + anchor = mOverflowButton; + } + + mOpenSubMenuId = subMenu.getItem().getItemId(); + mActionButtonPopup = new ActionButtonSubmenu(mContext, subMenu); + mActionButtonPopup.setAnchorView(anchor); + mActionButtonPopup.show(); + super.onSubMenuSelected(subMenu); + return true; + } + + private View findViewForItem(MenuItem item) { + final ViewGroup parent = (ViewGroup) mMenuView; + if (parent == null) return null; + + final int count = parent.getChildCount(); + for (int i = 0; i < count; i++) { + final View child = parent.getChildAt(i); + if (child instanceof MenuView.ItemView && + ((MenuView.ItemView) child).getItemData() == item) { + return child; + } + } + return null; + } + + /** + * Display the overflow menu if one is present. + * @return true if the overflow menu was shown, false otherwise. + */ + public boolean showOverflowMenu() { + if (mReserveOverflow && !isOverflowMenuShowing() && mMenu != null && mMenuView != null && + mPostedOpenRunnable == null && !mMenu.getNonActionItems().isEmpty()) { + OverflowPopup popup = new OverflowPopup(mContext, mMenu, mOverflowButton, true); + mPostedOpenRunnable = new OpenOverflowRunnable(popup); + // Post this for later; we might still need a layout for the anchor to be right. + ((View) mMenuView).post(mPostedOpenRunnable); + + // ActionMenuPresenter uses null as a callback argument here + // to indicate overflow is opening. + super.onSubMenuSelected(null); + + return true; + } + return false; + } + + /** + * Hide the overflow menu if it is currently showing. + * + * @return true if the overflow menu was hidden, false otherwise. + */ + public boolean hideOverflowMenu() { + if (mPostedOpenRunnable != null && mMenuView != null) { + ((View) mMenuView).removeCallbacks(mPostedOpenRunnable); + mPostedOpenRunnable = null; + return true; + } + + MenuPopupHelper popup = mOverflowPopup; + if (popup != null) { + popup.dismiss(); + return true; + } + return false; + } + + /** + * Dismiss all popup menus - overflow and submenus. + * @return true if popups were dismissed, false otherwise. (This can be because none were open.) + */ + public boolean dismissPopupMenus() { + boolean result = hideOverflowMenu(); + result |= hideSubMenus(); + return result; + } + + /** + * Dismiss all submenu popups. + * + * @return true if popups were dismissed, false otherwise. (This can be because none were open.) + */ + public boolean hideSubMenus() { + if (mActionButtonPopup != null) { + mActionButtonPopup.dismiss(); + return true; + } + return false; + } + + /** + * @return true if the overflow menu is currently showing + */ + public boolean isOverflowMenuShowing() { + return mOverflowPopup != null && mOverflowPopup.isShowing(); + } + + public boolean isOverflowMenuShowPending() { + return mPostedOpenRunnable != null || isOverflowMenuShowing(); + } + + /** + * @return true if space has been reserved in the action menu for an overflow item. + */ + public boolean isOverflowReserved() { + return mReserveOverflow; + } + + public boolean flagActionItems() { + final ArrayList<MenuItemImpl> visibleItems = mMenu.getVisibleItems(); + final int itemsSize = visibleItems.size(); + int maxActions = mMaxItems; + int widthLimit = mActionItemWidthLimit; + final int querySpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + final ViewGroup parent = (ViewGroup) mMenuView; + + int requiredItems = 0; + int requestedItems = 0; + int firstActionWidth = 0; + boolean hasOverflow = false; + for (int i = 0; i < itemsSize; i++) { + MenuItemImpl item = visibleItems.get(i); + if (item.requiresActionButton()) { + requiredItems++; + } else if (item.requestsActionButton()) { + requestedItems++; + } else { + hasOverflow = true; + } + if (mExpandedActionViewsExclusive && item.isActionViewExpanded()) { + // Overflow everything if we have an expanded action view and we're + // space constrained. + maxActions = 0; + } + } + + // Reserve a spot for the overflow item if needed. + if (mReserveOverflow && + (hasOverflow || requiredItems + requestedItems > maxActions)) { + maxActions--; + } + maxActions -= requiredItems; + + final SparseBooleanArray seenGroups = mActionButtonGroups; + seenGroups.clear(); + + int cellSize = 0; + int cellsRemaining = 0; + if (mStrictWidthLimit) { + cellsRemaining = widthLimit / mMinCellSize; + final int cellSizeRemaining = widthLimit % mMinCellSize; + cellSize = mMinCellSize + cellSizeRemaining / cellsRemaining; + } + + // Flag as many more requested items as will fit. + for (int i = 0; i < itemsSize; i++) { + MenuItemImpl item = visibleItems.get(i); + + if (item.requiresActionButton()) { + View v = getItemView(item, mScrapActionButtonView, parent); + if (mScrapActionButtonView == null) { + mScrapActionButtonView = v; + } + if (mStrictWidthLimit) { + cellsRemaining -= ActionMenuView.measureChildForCells(v, + cellSize, cellsRemaining, querySpec, 0); + } else { + v.measure(querySpec, querySpec); + } + final int measuredWidth = v.getMeasuredWidth(); + widthLimit -= measuredWidth; + if (firstActionWidth == 0) { + firstActionWidth = measuredWidth; + } + final int groupId = item.getGroupId(); + if (groupId != 0) { + seenGroups.put(groupId, true); + } + item.setIsActionButton(true); + } else if (item.requestsActionButton()) { + // Items in a group with other items that already have an action slot + // can break the max actions rule, but not the width limit. + final int groupId = item.getGroupId(); + final boolean inGroup = seenGroups.get(groupId); + boolean isAction = (maxActions > 0 || inGroup) && widthLimit > 0 && + (!mStrictWidthLimit || cellsRemaining > 0); + + if (isAction) { + View v = getItemView(item, mScrapActionButtonView, parent); + if (mScrapActionButtonView == null) { + mScrapActionButtonView = v; + } + if (mStrictWidthLimit) { + final int cells = ActionMenuView.measureChildForCells(v, + cellSize, cellsRemaining, querySpec, 0); + cellsRemaining -= cells; + if (cells == 0) { + isAction = false; + } + } else { + v.measure(querySpec, querySpec); + } + final int measuredWidth = v.getMeasuredWidth(); + widthLimit -= measuredWidth; + if (firstActionWidth == 0) { + firstActionWidth = measuredWidth; + } + + if (mStrictWidthLimit) { + isAction &= widthLimit >= 0; + } else { + // Did this push the entire first item past the limit? + isAction &= widthLimit + firstActionWidth > 0; + } + } + + if (isAction && groupId != 0) { + seenGroups.put(groupId, true); + } else if (inGroup) { + // We broke the width limit. Demote the whole group, they all overflow now. + seenGroups.put(groupId, false); + for (int j = 0; j < i; j++) { + MenuItemImpl areYouMyGroupie = visibleItems.get(j); + if (areYouMyGroupie.getGroupId() == groupId) { + // Give back the action slot + if (areYouMyGroupie.isActionButton()) maxActions++; + areYouMyGroupie.setIsActionButton(false); + } + } + } + + if (isAction) maxActions--; + + item.setIsActionButton(isAction); + } else { + // Neither requires nor requests an action button. + item.setIsActionButton(false); + } + } + return true; + } + + @Override + public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { + dismissPopupMenus(); + super.onCloseMenu(menu, allMenusAreClosing); + } + + @Override + public Parcelable onSaveInstanceState() { + SavedState state = new SavedState(); + state.openSubMenuId = mOpenSubMenuId; + return state; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + SavedState saved = (SavedState) state; + if (saved.openSubMenuId > 0) { + MenuItem item = mMenu.findItem(saved.openSubMenuId); + if (item != null) { + SubMenuBuilder subMenu = (SubMenuBuilder) item.getSubMenu(); + onSubMenuSelected(subMenu); + } + } + } + + @Override + public void onSubUiVisibilityChanged(boolean isVisible) { + if (isVisible) { + // Not a submenu, but treat it like one. + super.onSubMenuSelected(null); + } else { + mMenu.close(false); + } + } + + public void setMenuView(ActionMenuView menuView) { + mMenuView = menuView; + } + + private static class SavedState implements Parcelable { + public int openSubMenuId; + + SavedState() { + } + + SavedState(Parcel in) { + openSubMenuId = in.readInt(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(openSubMenuId); + } + + public static final Parcelable.Creator<SavedState> CREATOR + = new Parcelable.Creator<SavedState>() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + private class OverflowMenuButton extends ImageButton implements ActionMenuView.ActionMenuChildView { + public OverflowMenuButton(Context context) { + super(context, null, com.android.internal.R.attr.actionOverflowButtonStyle); + + setClickable(true); + setFocusable(true); + setVisibility(VISIBLE); + setEnabled(true); + + setOnTouchListener(new ForwardingListener(this) { + @Override + public ListPopupWindow getPopup() { + if (mOverflowPopup == null) { + return null; + } + + return mOverflowPopup.getPopup(); + } + + @Override + public boolean onForwardingStarted() { + showOverflowMenu(); + return true; + } + + @Override + public boolean onForwardingStopped() { + // Displaying the popup occurs asynchronously, so wait for + // the runnable to finish before deciding whether to stop + // forwarding. + if (mPostedOpenRunnable != null) { + return false; + } + + hideOverflowMenu(); + return true; + } + }); + } + + @Override + public boolean performClick() { + if (super.performClick()) { + return true; + } + + playSoundEffect(SoundEffectConstants.CLICK); + showOverflowMenu(); + return true; + } + + @Override + public boolean needsDividerBefore() { + return false; + } + + @Override + public boolean needsDividerAfter() { + return false; + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setCanOpenPopup(true); + } + } + + private class OverflowPopup extends MenuPopupHelper { + public OverflowPopup(Context context, MenuBuilder menu, View anchorView, + boolean overflowOnly) { + super(context, menu, anchorView, overflowOnly); + setGravity(Gravity.END); + setCallback(mPopupPresenterCallback); + } + + @Override + public void onDismiss() { + super.onDismiss(); + mMenu.close(); + mOverflowPopup = null; + } + } + + private class ActionButtonSubmenu extends MenuPopupHelper { + private SubMenuBuilder mSubMenu; + + public ActionButtonSubmenu(Context context, SubMenuBuilder subMenu) { + super(context, subMenu); + mSubMenu = subMenu; + + MenuItemImpl item = (MenuItemImpl) subMenu.getItem(); + if (!item.isActionButton()) { + // Give a reasonable anchor to nested submenus. + setAnchorView(mOverflowButton == null ? (View) mMenuView : mOverflowButton); + } + + setCallback(mPopupPresenterCallback); + + boolean preserveIconSpacing = false; + final int count = subMenu.size(); + for (int i = 0; i < count; i++) { + MenuItem childItem = subMenu.getItem(i); + if (childItem.isVisible() && childItem.getIcon() != null) { + preserveIconSpacing = true; + break; + } + } + setForceShowIcon(preserveIconSpacing); + } + + @Override + public void onDismiss() { + super.onDismiss(); + mActionButtonPopup = null; + mOpenSubMenuId = 0; + } + } + + private class PopupPresenterCallback implements Callback { + + @Override + public boolean onOpenSubMenu(MenuBuilder subMenu) { + if (subMenu == null) return false; + + mOpenSubMenuId = ((SubMenuBuilder) subMenu).getItem().getItemId(); + final Callback cb = getCallback(); + return cb != null ? cb.onOpenSubMenu(subMenu) : false; + } + + @Override + public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { + if (menu instanceof SubMenuBuilder) { + ((SubMenuBuilder) menu).getRootMenu().close(false); + } + final Callback cb = getCallback(); + if (cb != null) { + cb.onCloseMenu(menu, allMenusAreClosing); + } + } + } + + private class OpenOverflowRunnable implements Runnable { + private OverflowPopup mPopup; + + public OpenOverflowRunnable(OverflowPopup popup) { + mPopup = popup; + } + + public void run() { + mMenu.changeMenuMode(); + final View menuView = (View) mMenuView; + if (menuView != null && menuView.getWindowToken() != null && mPopup.tryShow()) { + mOverflowPopup = mPopup; + } + mPostedOpenRunnable = null; + } + } + + private class ActionMenuPopupCallback extends ActionMenuItemView.PopupCallback { + @Override + public ListPopupWindow getPopup() { + return mActionButtonPopup != null ? mActionButtonPopup.getPopup() : null; + } + } +} diff --git a/core/java/android/widget/ActionMenuView.java b/core/java/android/widget/ActionMenuView.java new file mode 100644 index 0000000..3975edf --- /dev/null +++ b/core/java/android/widget/ActionMenuView.java @@ -0,0 +1,696 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.widget; + +import android.content.Context; +import android.content.res.Configuration; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewDebug; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import com.android.internal.view.menu.ActionMenuItemView; +import com.android.internal.view.menu.MenuBuilder; +import com.android.internal.view.menu.MenuItemImpl; +import com.android.internal.view.menu.MenuPresenter; +import com.android.internal.view.menu.MenuView; + +/** + * ActionMenuView is a presentation of a series of menu options as a View. It provides + * several top level options as action buttons while spilling remaining options over as + * items in an overflow menu. This allows applications to present packs of actions inline with + * specific or repeating content. + */ +public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvoker, MenuView { + private static final String TAG = "ActionMenuView"; + + static final int MIN_CELL_SIZE = 56; // dips + static final int GENERATED_ITEM_PADDING = 4; // dips + + private MenuBuilder mMenu; + + private boolean mReserveOverflow; + private ActionMenuPresenter mPresenter; + private boolean mFormatItems; + private int mFormatItemsWidth; + private int mMinCellSize; + private int mGeneratedItemPadding; + + private OnMenuItemClickListener mOnMenuItemClickListener; + + public ActionMenuView(Context context) { + this(context, null); + } + + public ActionMenuView(Context context, AttributeSet attrs) { + super(context, attrs); + setBaselineAligned(false); + final float density = context.getResources().getDisplayMetrics().density; + mMinCellSize = (int) (MIN_CELL_SIZE * density); + mGeneratedItemPadding = (int) (GENERATED_ITEM_PADDING * density); + } + + /** @hide */ + public void setPresenter(ActionMenuPresenter presenter) { + mPresenter = presenter; + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + mPresenter.updateMenuView(false); + + if (mPresenter != null && mPresenter.isOverflowMenuShowing()) { + mPresenter.hideOverflowMenu(); + mPresenter.showOverflowMenu(); + } + } + + public void setOnMenuItemClickListener(OnMenuItemClickListener listener) { + mOnMenuItemClickListener = listener; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // If we've been given an exact size to match, apply special formatting during layout. + final boolean wasFormatted = mFormatItems; + mFormatItems = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY; + + if (wasFormatted != mFormatItems) { + mFormatItemsWidth = 0; // Reset this when switching modes + } + + // Special formatting can change whether items can fit as action buttons. + // Kick the menu and update presenters when this changes. + final int widthSize = MeasureSpec.getSize(widthMeasureSpec); + if (mFormatItems && mMenu != null && widthSize != mFormatItemsWidth) { + mFormatItemsWidth = widthSize; + mMenu.onItemsChanged(true); + } + + final int childCount = getChildCount(); + if (mFormatItems && childCount > 0) { + onMeasureExactFormat(widthMeasureSpec, heightMeasureSpec); + } else { + // Previous measurement at exact format may have set margins - reset them. + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + lp.leftMargin = lp.rightMargin = 0; + } + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + } + + private void onMeasureExactFormat(int widthMeasureSpec, int heightMeasureSpec) { + // We already know the width mode is EXACTLY if we're here. + final int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + final int widthPadding = getPaddingLeft() + getPaddingRight(); + final int heightPadding = getPaddingTop() + getPaddingBottom(); + + final int itemHeightSpec = getChildMeasureSpec(heightMeasureSpec, heightPadding, + ViewGroup.LayoutParams.WRAP_CONTENT); + + widthSize -= widthPadding; + + // Divide the view into cells. + final int cellCount = widthSize / mMinCellSize; + final int cellSizeRemaining = widthSize % mMinCellSize; + + if (cellCount == 0) { + // Give up, nothing fits. + setMeasuredDimension(widthSize, 0); + return; + } + + final int cellSize = mMinCellSize + cellSizeRemaining / cellCount; + + int cellsRemaining = cellCount; + int maxChildHeight = 0; + int maxCellsUsed = 0; + int expandableItemCount = 0; + int visibleItemCount = 0; + boolean hasOverflow = false; + + // This is used as a bitfield to locate the smallest items present. Assumes childCount < 64. + long smallestItemsAt = 0; + + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (child.getVisibility() == GONE) continue; + + final boolean isGeneratedItem = child instanceof ActionMenuItemView; + visibleItemCount++; + + if (isGeneratedItem) { + // Reset padding for generated menu item views; it may change below + // and views are recycled. + child.setPadding(mGeneratedItemPadding, 0, mGeneratedItemPadding, 0); + } + + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + lp.expanded = false; + lp.extraPixels = 0; + lp.cellsUsed = 0; + lp.expandable = false; + lp.leftMargin = 0; + lp.rightMargin = 0; + lp.preventEdgeOffset = isGeneratedItem && ((ActionMenuItemView) child).hasText(); + + // Overflow always gets 1 cell. No more, no less. + final int cellsAvailable = lp.isOverflowButton ? 1 : cellsRemaining; + + final int cellsUsed = measureChildForCells(child, cellSize, cellsAvailable, + itemHeightSpec, heightPadding); + + maxCellsUsed = Math.max(maxCellsUsed, cellsUsed); + if (lp.expandable) expandableItemCount++; + if (lp.isOverflowButton) hasOverflow = true; + + cellsRemaining -= cellsUsed; + maxChildHeight = Math.max(maxChildHeight, child.getMeasuredHeight()); + if (cellsUsed == 1) smallestItemsAt |= (1 << i); + } + + // When we have overflow and a single expanded (text) item, we want to try centering it + // visually in the available space even though overflow consumes some of it. + final boolean centerSingleExpandedItem = hasOverflow && visibleItemCount == 2; + + // Divide space for remaining cells if we have items that can expand. + // Try distributing whole leftover cells to smaller items first. + + boolean needsExpansion = false; + while (expandableItemCount > 0 && cellsRemaining > 0) { + int minCells = Integer.MAX_VALUE; + long minCellsAt = 0; // Bit locations are indices of relevant child views + int minCellsItemCount = 0; + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + // Don't try to expand items that shouldn't. + if (!lp.expandable) continue; + + // Mark indices of children that can receive an extra cell. + if (lp.cellsUsed < minCells) { + minCells = lp.cellsUsed; + minCellsAt = 1 << i; + minCellsItemCount = 1; + } else if (lp.cellsUsed == minCells) { + minCellsAt |= 1 << i; + minCellsItemCount++; + } + } + + // Items that get expanded will always be in the set of smallest items when we're done. + smallestItemsAt |= minCellsAt; + + if (minCellsItemCount > cellsRemaining) break; // Couldn't expand anything evenly. Stop. + + // We have enough cells, all minimum size items will be incremented. + minCells++; + + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if ((minCellsAt & (1 << i)) == 0) { + // If this item is already at our small item count, mark it for later. + if (lp.cellsUsed == minCells) smallestItemsAt |= 1 << i; + continue; + } + + if (centerSingleExpandedItem && lp.preventEdgeOffset && cellsRemaining == 1) { + // Add padding to this item such that it centers. + child.setPadding(mGeneratedItemPadding + cellSize, 0, mGeneratedItemPadding, 0); + } + lp.cellsUsed++; + lp.expanded = true; + cellsRemaining--; + } + + needsExpansion = true; + } + + // Divide any space left that wouldn't divide along cell boundaries + // evenly among the smallest items + + final boolean singleItem = !hasOverflow && visibleItemCount == 1; + if (cellsRemaining > 0 && smallestItemsAt != 0 && + (cellsRemaining < visibleItemCount - 1 || singleItem || maxCellsUsed > 1)) { + float expandCount = Long.bitCount(smallestItemsAt); + + if (!singleItem) { + // The items at the far edges may only expand by half in order to pin to either side. + if ((smallestItemsAt & 1) != 0) { + LayoutParams lp = (LayoutParams) getChildAt(0).getLayoutParams(); + if (!lp.preventEdgeOffset) expandCount -= 0.5f; + } + if ((smallestItemsAt & (1 << (childCount - 1))) != 0) { + LayoutParams lp = ((LayoutParams) getChildAt(childCount - 1).getLayoutParams()); + if (!lp.preventEdgeOffset) expandCount -= 0.5f; + } + } + + final int extraPixels = expandCount > 0 ? + (int) (cellsRemaining * cellSize / expandCount) : 0; + + for (int i = 0; i < childCount; i++) { + if ((smallestItemsAt & (1 << i)) == 0) continue; + + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (child instanceof ActionMenuItemView) { + // If this is one of our views, expand and measure at the larger size. + lp.extraPixels = extraPixels; + lp.expanded = true; + if (i == 0 && !lp.preventEdgeOffset) { + // First item gets part of its new padding pushed out of sight. + // The last item will get this implicitly from layout. + lp.leftMargin = -extraPixels / 2; + } + needsExpansion = true; + } else if (lp.isOverflowButton) { + lp.extraPixels = extraPixels; + lp.expanded = true; + lp.rightMargin = -extraPixels / 2; + needsExpansion = true; + } else { + // If we don't know what it is, give it some margins instead + // and let it center within its space. We still want to pin + // against the edges. + if (i != 0) { + lp.leftMargin = extraPixels / 2; + } + if (i != childCount - 1) { + lp.rightMargin = extraPixels / 2; + } + } + } + + cellsRemaining = 0; + } + + // Remeasure any items that have had extra space allocated to them. + if (needsExpansion) { + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if (!lp.expanded) continue; + + final int width = lp.cellsUsed * cellSize + lp.extraPixels; + child.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + itemHeightSpec); + } + } + + if (heightMode != MeasureSpec.EXACTLY) { + heightSize = maxChildHeight; + } + + setMeasuredDimension(widthSize, heightSize); + } + + /** + * Measure a child view to fit within cell-based formatting. The child's width + * will be measured to a whole multiple of cellSize. + * + * <p>Sets the expandable and cellsUsed fields of LayoutParams. + * + * @param child Child to measure + * @param cellSize Size of one cell + * @param cellsRemaining Number of cells remaining that this view can expand to fill + * @param parentHeightMeasureSpec MeasureSpec used by the parent view + * @param parentHeightPadding Padding present in the parent view + * @return Number of cells this child was measured to occupy + */ + static int measureChildForCells(View child, int cellSize, int cellsRemaining, + int parentHeightMeasureSpec, int parentHeightPadding) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + final int childHeightSize = MeasureSpec.getSize(parentHeightMeasureSpec) - + parentHeightPadding; + final int childHeightMode = MeasureSpec.getMode(parentHeightMeasureSpec); + final int childHeightSpec = MeasureSpec.makeMeasureSpec(childHeightSize, childHeightMode); + + final ActionMenuItemView itemView = child instanceof ActionMenuItemView ? + (ActionMenuItemView) child : null; + final boolean hasText = itemView != null && itemView.hasText(); + + int cellsUsed = 0; + if (cellsRemaining > 0 && (!hasText || cellsRemaining >= 2)) { + final int childWidthSpec = MeasureSpec.makeMeasureSpec( + cellSize * cellsRemaining, MeasureSpec.AT_MOST); + child.measure(childWidthSpec, childHeightSpec); + + final int measuredWidth = child.getMeasuredWidth(); + cellsUsed = measuredWidth / cellSize; + if (measuredWidth % cellSize != 0) cellsUsed++; + if (hasText && cellsUsed < 2) cellsUsed = 2; + } + + final boolean expandable = !lp.isOverflowButton && hasText; + lp.expandable = expandable; + + lp.cellsUsed = cellsUsed; + final int targetWidth = cellsUsed * cellSize; + child.measure(MeasureSpec.makeMeasureSpec(targetWidth, MeasureSpec.EXACTLY), + childHeightSpec); + return cellsUsed; + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + if (!mFormatItems) { + super.onLayout(changed, left, top, right, bottom); + return; + } + + final int childCount = getChildCount(); + final int midVertical = (top + bottom) / 2; + final int dividerWidth = getDividerWidth(); + int overflowWidth = 0; + int nonOverflowWidth = 0; + int nonOverflowCount = 0; + int widthRemaining = right - left - getPaddingRight() - getPaddingLeft(); + boolean hasOverflow = false; + final boolean isLayoutRtl = isLayoutRtl(); + for (int i = 0; i < childCount; i++) { + final View v = getChildAt(i); + if (v.getVisibility() == GONE) { + continue; + } + + LayoutParams p = (LayoutParams) v.getLayoutParams(); + if (p.isOverflowButton) { + overflowWidth = v.getMeasuredWidth(); + if (hasDividerBeforeChildAt(i)) { + overflowWidth += dividerWidth; + } + + int height = v.getMeasuredHeight(); + int r; + int l; + if (isLayoutRtl) { + l = getPaddingLeft() + p.leftMargin; + r = l + overflowWidth; + } else { + r = getWidth() - getPaddingRight() - p.rightMargin; + l = r - overflowWidth; + } + int t = midVertical - (height / 2); + int b = t + height; + v.layout(l, t, r, b); + + widthRemaining -= overflowWidth; + hasOverflow = true; + } else { + final int size = v.getMeasuredWidth() + p.leftMargin + p.rightMargin; + nonOverflowWidth += size; + widthRemaining -= size; + if (hasDividerBeforeChildAt(i)) { + nonOverflowWidth += dividerWidth; + } + nonOverflowCount++; + } + } + + if (childCount == 1 && !hasOverflow) { + // Center a single child + final View v = getChildAt(0); + final int width = v.getMeasuredWidth(); + final int height = v.getMeasuredHeight(); + final int midHorizontal = (right - left) / 2; + final int l = midHorizontal - width / 2; + final int t = midVertical - height / 2; + v.layout(l, t, l + width, t + height); + return; + } + + final int spacerCount = nonOverflowCount - (hasOverflow ? 0 : 1); + final int spacerSize = Math.max(0, spacerCount > 0 ? widthRemaining / spacerCount : 0); + + if (isLayoutRtl) { + int startRight = getWidth() - getPaddingRight(); + for (int i = 0; i < childCount; i++) { + final View v = getChildAt(i); + final LayoutParams lp = (LayoutParams) v.getLayoutParams(); + if (v.getVisibility() == GONE || lp.isOverflowButton) { + continue; + } + + startRight -= lp.rightMargin; + int width = v.getMeasuredWidth(); + int height = v.getMeasuredHeight(); + int t = midVertical - height / 2; + v.layout(startRight - width, t, startRight, t + height); + startRight -= width + lp.leftMargin + spacerSize; + } + } else { + int startLeft = getPaddingLeft(); + for (int i = 0; i < childCount; i++) { + final View v = getChildAt(i); + final LayoutParams lp = (LayoutParams) v.getLayoutParams(); + if (v.getVisibility() == GONE || lp.isOverflowButton) { + continue; + } + + startLeft += lp.leftMargin; + int width = v.getMeasuredWidth(); + int height = v.getMeasuredHeight(); + int t = midVertical - height / 2; + v.layout(startLeft, t, startLeft + width, t + height); + startLeft += width + lp.rightMargin + spacerSize; + } + } + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mPresenter.dismissPopupMenus(); + } + + /** @hide */ + public boolean isOverflowReserved() { + return mReserveOverflow; + } + + /** @hide */ + public void setOverflowReserved(boolean reserveOverflow) { + mReserveOverflow = reserveOverflow; + } + + @Override + protected LayoutParams generateDefaultLayoutParams() { + LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT); + params.gravity = Gravity.CENTER_VERTICAL; + return params; + } + + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs); + } + + @Override + protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + if (p != null) { + final LayoutParams result = p instanceof LayoutParams + ? new LayoutParams((LayoutParams) p) + : new LayoutParams(p); + if (result.gravity <= Gravity.NO_GRAVITY) { + result.gravity = Gravity.CENTER_VERTICAL; + } + return result; + } + return generateDefaultLayoutParams(); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p != null && p instanceof LayoutParams; + } + + /** @hide */ + public LayoutParams generateOverflowButtonLayoutParams() { + LayoutParams result = generateDefaultLayoutParams(); + result.isOverflowButton = true; + return result; + } + + /** @hide */ + public boolean invokeItem(MenuItemImpl item) { + return mMenu.performItemAction(item, 0); + } + + /** @hide */ + public int getWindowAnimations() { + return 0; + } + + /** @hide */ + public void initialize(MenuBuilder menu) { + mMenu = menu; + } + + /** + * Returns the Menu object that this ActionMenuView is currently presenting. + * + * <p>Applications should use this method to obtain the ActionMenuView's Menu object + * and inflate or add content to it as necessary.</p> + * + * @return the Menu presented by this view + */ + public Menu getMenu() { + if (mMenu == null) { + final Context context = getContext(); + mMenu = new MenuBuilder(context); + mMenu.setCallback(new MenuBuilderCallback()); + mPresenter = new ActionMenuPresenter(context); + mPresenter.setMenuView(this); + mPresenter.setCallback(new ActionMenuPresenterCallback()); + mMenu.addMenuPresenter(mPresenter); + } + + return mMenu; + } + + /** + * @hide Private LinearLayout (superclass) API. Un-hide if LinearLayout API is made public. + */ + @Override + protected boolean hasDividerBeforeChildAt(int childIndex) { + if (childIndex == 0) { + return false; + } + final View childBefore = getChildAt(childIndex - 1); + final View child = getChildAt(childIndex); + boolean result = false; + if (childIndex < getChildCount() && childBefore instanceof ActionMenuChildView) { + result |= ((ActionMenuChildView) childBefore).needsDividerAfter(); + } + if (childIndex > 0 && child instanceof ActionMenuChildView) { + result |= ((ActionMenuChildView) child).needsDividerBefore(); + } + return result; + } + + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + return false; + } + + /** + * Interface responsible for receiving menu item click events if the items themselves + * do not have individual item click listeners. + */ + public interface OnMenuItemClickListener { + /** + * This method will be invoked when a menu item is clicked if the item itself did + * not already handle the event. + * + * @param item {@link MenuItem} that was clicked + * @return <code>true</code> if the event was handled, <code>false</code> otherwise. + */ + public boolean onMenuItemClick(MenuItem item); + } + + private class MenuBuilderCallback implements MenuBuilder.Callback { + @Override + public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) { + return mOnMenuItemClickListener != null && + mOnMenuItemClickListener.onMenuItemClick(item); + } + + @Override + public void onMenuModeChange(MenuBuilder menu) { + } + } + + private class ActionMenuPresenterCallback implements ActionMenuPresenter.Callback { + @Override + public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { + } + + @Override + public boolean onOpenSubMenu(MenuBuilder subMenu) { + return false; + } + } + + /** @hide */ + public interface ActionMenuChildView { + public boolean needsDividerBefore(); + public boolean needsDividerAfter(); + } + + public static class LayoutParams extends LinearLayout.LayoutParams { + /** @hide */ + @ViewDebug.ExportedProperty(category = "layout") + public boolean isOverflowButton; + + /** @hide */ + @ViewDebug.ExportedProperty(category = "layout") + public int cellsUsed; + + /** @hide */ + @ViewDebug.ExportedProperty(category = "layout") + public int extraPixels; + + /** @hide */ + @ViewDebug.ExportedProperty(category = "layout") + public boolean expandable; + + /** @hide */ + @ViewDebug.ExportedProperty(category = "layout") + public boolean preventEdgeOffset; + + /** @hide */ + public boolean expanded; + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + } + + public LayoutParams(ViewGroup.LayoutParams other) { + super(other); + } + + public LayoutParams(LayoutParams other) { + super((LinearLayout.LayoutParams) other); + isOverflowButton = other.isOverflowButton; + } + + public LayoutParams(int width, int height) { + super(width, height); + isOverflowButton = false; + } + + /** @hide */ + public LayoutParams(int width, int height, boolean isOverflowButton) { + super(width, height); + this.isOverflowButton = isOverflowButton; + } + } +} diff --git a/core/java/android/widget/ActivityChooserView.java b/core/java/android/widget/ActivityChooserView.java index 8612964..f9af2f9 100644 --- a/core/java/android/widget/ActivityChooserView.java +++ b/core/java/android/widget/ActivityChooserView.java @@ -18,7 +18,6 @@ package android.widget; import com.android.internal.R; -import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; @@ -31,7 +30,6 @@ import android.util.AttributeSet; import android.util.Log; import android.view.ActionProvider; import android.view.LayoutInflater; -import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; @@ -204,13 +202,32 @@ public class ActivityChooserView extends ViewGroup implements ActivityChooserMod * * @param context The application environment. * @param attrs A collection of attributes. - * @param defStyle The default style to apply to this view. + * @param defStyleAttr An attribute in the current theme that contains a + * reference to a style resource that supplies default values for + * the view. Can be 0 to not look for defaults. */ - public ActivityChooserView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public ActivityChooserView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + /** + * Create a new instance. + * + * @param context The application environment. + * @param attrs A collection of attributes. + * @param defStyleAttr An attribute in the current theme that contains a + * reference to a style resource that supplies default values for + * the view. Can be 0 to not look for defaults. + * @param defStyleRes A resource identifier of a style resource that + * supplies default values for the view, used only if + * defStyleAttr is 0 or can not be found in the theme. Can be 0 + * to not look for defaults. + */ + public ActivityChooserView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); TypedArray attributesArray = context.obtainStyledAttributes(attrs, - R.styleable.ActivityChooserView, defStyle, 0); + R.styleable.ActivityChooserView, defStyleAttr, defStyleRes); mInitialActivityCount = attributesArray.getInt( R.styleable.ActivityChooserView_initialActivityCount, diff --git a/core/java/android/widget/AdapterView.java b/core/java/android/widget/AdapterView.java index a06344f..1da22ca 100644 --- a/core/java/android/widget/AdapterView.java +++ b/core/java/android/widget/AdapterView.java @@ -223,15 +223,19 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { boolean mBlockLayoutRequests = false; public AdapterView(Context context) { - super(context); + this(context, null); } public AdapterView(Context context, AttributeSet attrs) { - super(context, attrs); + this(context, attrs, 0); } - public AdapterView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public AdapterView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public AdapterView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); // If not explicitly specified this view is important for accessibility. if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { diff --git a/core/java/android/widget/AdapterViewAnimator.java b/core/java/android/widget/AdapterViewAnimator.java index 90e949a..1bc2f4b 100644 --- a/core/java/android/widget/AdapterViewAnimator.java +++ b/core/java/android/widget/AdapterViewAnimator.java @@ -173,10 +173,15 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> } public AdapterViewAnimator(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); + this(context, attrs, defStyleAttr, 0); + } + + public AdapterViewAnimator( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.AdapterViewAnimator, defStyleAttr, 0); + final TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.AdapterViewAnimator, defStyleAttr, defStyleRes); int resource = a.getResourceId( com.android.internal.R.styleable.AdapterViewAnimator_inAnimation, 0); if (resource > 0) { diff --git a/core/java/android/widget/AdapterViewFlipper.java b/core/java/android/widget/AdapterViewFlipper.java index aea029b..3b026bd 100644 --- a/core/java/android/widget/AdapterViewFlipper.java +++ b/core/java/android/widget/AdapterViewFlipper.java @@ -59,10 +59,19 @@ public class AdapterViewFlipper extends AdapterViewAnimator { } public AdapterViewFlipper(Context context, AttributeSet attrs) { - super(context, attrs); + this(context, attrs, 0); + } + + public AdapterViewFlipper(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public AdapterViewFlipper( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.AdapterViewFlipper); + final TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.AdapterViewFlipper, defStyleAttr, defStyleRes); mFlipInterval = a.getInt( com.android.internal.R.styleable.AdapterViewFlipper_flipInterval, DEFAULT_INTERVAL); mAutoStart = a.getBoolean( diff --git a/core/java/android/widget/AnalogClock.java b/core/java/android/widget/AnalogClock.java index c7da818..5b80648 100644 --- a/core/java/android/widget/AnalogClock.java +++ b/core/java/android/widget/AnalogClock.java @@ -67,27 +67,30 @@ public class AnalogClock extends View { this(context, attrs, 0); } - public AnalogClock(Context context, AttributeSet attrs, - int defStyle) { - super(context, attrs, defStyle); - Resources r = mContext.getResources(); - TypedArray a = - context.obtainStyledAttributes( - attrs, com.android.internal.R.styleable.AnalogClock, defStyle, 0); + public AnalogClock(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public AnalogClock(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + final Resources r = context.getResources(); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.AnalogClock, defStyleAttr, defStyleRes); mDial = a.getDrawable(com.android.internal.R.styleable.AnalogClock_dial); if (mDial == null) { - mDial = r.getDrawable(com.android.internal.R.drawable.clock_dial); + mDial = context.getDrawable(com.android.internal.R.drawable.clock_dial); } mHourHand = a.getDrawable(com.android.internal.R.styleable.AnalogClock_hand_hour); if (mHourHand == null) { - mHourHand = r.getDrawable(com.android.internal.R.drawable.clock_hand_hour); + mHourHand = context.getDrawable(com.android.internal.R.drawable.clock_hand_hour); } mMinuteHand = a.getDrawable(com.android.internal.R.styleable.AnalogClock_hand_minute); if (mMinuteHand == null) { - mMinuteHand = r.getDrawable(com.android.internal.R.drawable.clock_hand_minute); + mMinuteHand = context.getDrawable(com.android.internal.R.drawable.clock_hand_minute); } mCalendar = new Time(); diff --git a/core/java/android/widget/AppSecurityPermissions.java b/core/java/android/widget/AppSecurityPermissions.java index 34cfea5..10e56c7 100644 --- a/core/java/android/widget/AppSecurityPermissions.java +++ b/core/java/android/widget/AppSecurityPermissions.java @@ -322,7 +322,7 @@ public class AppSecurityPermissions { CharSequence grpName, CharSequence description, boolean dangerous) { LayoutInflater inflater = (LayoutInflater)context.getSystemService( Context.LAYOUT_INFLATER_SERVICE); - Drawable icon = context.getResources().getDrawable(dangerous + Drawable icon = context.getDrawable(dangerous ? R.drawable.ic_bullet_key_permission : R.drawable.ic_text_dot); return getPermissionItemViewOld(context, inflater, grpName, description, dangerous, icon); diff --git a/core/java/android/widget/AutoCompleteTextView.java b/core/java/android/widget/AutoCompleteTextView.java index f0eb94f..eb232fd 100644 --- a/core/java/android/widget/AutoCompleteTextView.java +++ b/core/java/android/widget/AutoCompleteTextView.java @@ -133,17 +133,21 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe this(context, attrs, com.android.internal.R.attr.autoCompleteTextViewStyle); } - public AutoCompleteTextView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public AutoCompleteTextView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public AutoCompleteTextView( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); mPopup = new ListPopupWindow(context, attrs, com.android.internal.R.attr.autoCompleteTextViewStyle); mPopup.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); mPopup.setPromptPosition(ListPopupWindow.POSITION_PROMPT_BELOW); - TypedArray a = - context.obtainStyledAttributes( - attrs, com.android.internal.R.styleable.AutoCompleteTextView, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.AutoCompleteTextView, defStyleAttr, defStyleRes); mThreshold = a.getInt( R.styleable.AutoCompleteTextView_completionThreshold, 2); @@ -362,7 +366,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * @attr ref android.R.styleable#PopupWindow_popupBackground */ public void setDropDownBackgroundResource(int id) { - mPopup.setBackgroundDrawable(getResources().getDrawable(id)); + mPopup.setBackgroundDrawable(getContext().getDrawable(id)); } /** diff --git a/core/java/android/widget/Button.java b/core/java/android/widget/Button.java index 2ac56ac..1663620 100644 --- a/core/java/android/widget/Button.java +++ b/core/java/android/widget/Button.java @@ -103,8 +103,12 @@ public class Button extends TextView { this(context, attrs, com.android.internal.R.attr.buttonStyle); } - public Button(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public Button(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public Button(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); } @Override diff --git a/core/java/android/widget/CalendarView.java b/core/java/android/widget/CalendarView.java index e90b460..ea60abb 100644 --- a/core/java/android/widget/CalendarView.java +++ b/core/java/android/widget/CalendarView.java @@ -80,234 +80,7 @@ public class CalendarView extends FrameLayout { */ private static final String LOG_TAG = CalendarView.class.getSimpleName(); - /** - * Default value whether to show week number. - */ - private static final boolean DEFAULT_SHOW_WEEK_NUMBER = true; - - /** - * The number of milliseconds in a day.e - */ - private static final long MILLIS_IN_DAY = 86400000L; - - /** - * The number of day in a week. - */ - private static final int DAYS_PER_WEEK = 7; - - /** - * The number of milliseconds in a week. - */ - private static final long MILLIS_IN_WEEK = DAYS_PER_WEEK * MILLIS_IN_DAY; - - /** - * Affects when the month selection will change while scrolling upe - */ - private static final int SCROLL_HYST_WEEKS = 2; - - /** - * How long the GoTo fling animation should last. - */ - private static final int GOTO_SCROLL_DURATION = 1000; - - /** - * The duration of the adjustment upon a user scroll in milliseconds. - */ - private static final int ADJUSTMENT_SCROLL_DURATION = 500; - - /** - * How long to wait after receiving an onScrollStateChanged notification - * before acting on it. - */ - private static final int SCROLL_CHANGE_DELAY = 40; - - /** - * String for parsing dates. - */ - private static final String DATE_FORMAT = "MM/dd/yyyy"; - - /** - * The default minimal date. - */ - private static final String DEFAULT_MIN_DATE = "01/01/1900"; - - /** - * The default maximal date. - */ - private static final String DEFAULT_MAX_DATE = "01/01/2100"; - - private static final int DEFAULT_SHOWN_WEEK_COUNT = 6; - - private static final int DEFAULT_DATE_TEXT_SIZE = 14; - - private static final int UNSCALED_SELECTED_DATE_VERTICAL_BAR_WIDTH = 6; - - private static final int UNSCALED_WEEK_MIN_VISIBLE_HEIGHT = 12; - - private static final int UNSCALED_LIST_SCROLL_TOP_OFFSET = 2; - - private static final int UNSCALED_BOTTOM_BUFFER = 20; - - private static final int UNSCALED_WEEK_SEPARATOR_LINE_WIDTH = 1; - - private static final int DEFAULT_WEEK_DAY_TEXT_APPEARANCE_RES_ID = -1; - - private final int mWeekSeperatorLineWidth; - - private int mDateTextSize; - - private Drawable mSelectedDateVerticalBar; - - private final int mSelectedDateVerticalBarWidth; - - private int mSelectedWeekBackgroundColor; - - private int mFocusedMonthDateColor; - - private int mUnfocusedMonthDateColor; - - private int mWeekSeparatorLineColor; - - private int mWeekNumberColor; - - private int mWeekDayTextAppearanceResId; - - private int mDateTextAppearanceResId; - - /** - * The top offset of the weeks list. - */ - private int mListScrollTopOffset = 2; - - /** - * The visible height of a week view. - */ - private int mWeekMinVisibleHeight = 12; - - /** - * The visible height of a week view. - */ - private int mBottomBuffer = 20; - - /** - * The number of shown weeks. - */ - private int mShownWeekCount; - - /** - * Flag whether to show the week number. - */ - private boolean mShowWeekNumber; - - /** - * The number of day per week to be shown. - */ - private int mDaysPerWeek = 7; - - /** - * The friction of the week list while flinging. - */ - private float mFriction = .05f; - - /** - * Scale for adjusting velocity of the week list while flinging. - */ - private float mVelocityScale = 0.333f; - - /** - * The adapter for the weeks list. - */ - private WeeksAdapter mAdapter; - - /** - * The weeks list. - */ - private ListView mListView; - - /** - * The name of the month to display. - */ - private TextView mMonthName; - - /** - * The header with week day names. - */ - private ViewGroup mDayNamesHeader; - - /** - * Cached labels for the week names header. - */ - private String[] mDayLabels; - - /** - * The first day of the week. - */ - private int mFirstDayOfWeek; - - /** - * Which month should be displayed/highlighted [0-11]. - */ - private int mCurrentMonthDisplayed = -1; - - /** - * Used for tracking during a scroll. - */ - private long mPreviousScrollPosition; - - /** - * Used for tracking which direction the view is scrolling. - */ - private boolean mIsScrollingUp = false; - - /** - * The previous scroll state of the weeks ListView. - */ - private int mPreviousScrollState = OnScrollListener.SCROLL_STATE_IDLE; - - /** - * The current scroll state of the weeks ListView. - */ - private int mCurrentScrollState = OnScrollListener.SCROLL_STATE_IDLE; - - /** - * Listener for changes in the selected day. - */ - private OnDateChangeListener mOnDateChangeListener; - - /** - * Command for adjusting the position after a scroll/fling. - */ - private ScrollStateRunnable mScrollStateChangedRunnable = new ScrollStateRunnable(); - - /** - * Temporary instance to avoid multiple instantiations. - */ - private Calendar mTempDate; - - /** - * The first day of the focused month. - */ - private Calendar mFirstDayOfMonth; - - /** - * The start date of the range supported by this picker. - */ - private Calendar mMinDate; - - /** - * The end date of the range supported by this picker. - */ - private Calendar mMaxDate; - - /** - * Date format for parsing dates. - */ - private final java.text.DateFormat mDateFormat = new SimpleDateFormat(DATE_FORMAT); - - /** - * The current locale. - */ - private Locale mCurrentLocale; + private CalendarViewDelegate mDelegate; /** * The callback used to indicate the user changes the date. @@ -330,91 +103,17 @@ public class CalendarView extends FrameLayout { } public CalendarView(Context context, AttributeSet attrs) { - this(context, attrs, 0); + this(context, attrs, R.attr.calendarViewStyle); } - public CalendarView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, 0); - - // initialization based on locale - setCurrentLocale(Locale.getDefault()); - - TypedArray attributesArray = context.obtainStyledAttributes(attrs, R.styleable.CalendarView, - R.attr.calendarViewStyle, 0); - mShowWeekNumber = attributesArray.getBoolean(R.styleable.CalendarView_showWeekNumber, - DEFAULT_SHOW_WEEK_NUMBER); - mFirstDayOfWeek = attributesArray.getInt(R.styleable.CalendarView_firstDayOfWeek, - LocaleData.get(Locale.getDefault()).firstDayOfWeek); - String minDate = attributesArray.getString(R.styleable.CalendarView_minDate); - if (TextUtils.isEmpty(minDate) || !parseDate(minDate, mMinDate)) { - parseDate(DEFAULT_MIN_DATE, mMinDate); - } - String maxDate = attributesArray.getString(R.styleable.CalendarView_maxDate); - if (TextUtils.isEmpty(maxDate) || !parseDate(maxDate, mMaxDate)) { - parseDate(DEFAULT_MAX_DATE, mMaxDate); - } - if (mMaxDate.before(mMinDate)) { - throw new IllegalArgumentException("Max date cannot be before min date."); - } - mShownWeekCount = attributesArray.getInt(R.styleable.CalendarView_shownWeekCount, - DEFAULT_SHOWN_WEEK_COUNT); - mSelectedWeekBackgroundColor = attributesArray.getColor( - R.styleable.CalendarView_selectedWeekBackgroundColor, 0); - mFocusedMonthDateColor = attributesArray.getColor( - R.styleable.CalendarView_focusedMonthDateColor, 0); - mUnfocusedMonthDateColor = attributesArray.getColor( - R.styleable.CalendarView_unfocusedMonthDateColor, 0); - mWeekSeparatorLineColor = attributesArray.getColor( - R.styleable.CalendarView_weekSeparatorLineColor, 0); - mWeekNumberColor = attributesArray.getColor(R.styleable.CalendarView_weekNumberColor, 0); - mSelectedDateVerticalBar = attributesArray.getDrawable( - R.styleable.CalendarView_selectedDateVerticalBar); - - mDateTextAppearanceResId = attributesArray.getResourceId( - R.styleable.CalendarView_dateTextAppearance, R.style.TextAppearance_Small); - updateDateTextSize(); - - mWeekDayTextAppearanceResId = attributesArray.getResourceId( - R.styleable.CalendarView_weekDayTextAppearance, - DEFAULT_WEEK_DAY_TEXT_APPEARANCE_RES_ID); - attributesArray.recycle(); - - DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); - mWeekMinVisibleHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, - UNSCALED_WEEK_MIN_VISIBLE_HEIGHT, displayMetrics); - mListScrollTopOffset = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, - UNSCALED_LIST_SCROLL_TOP_OFFSET, displayMetrics); - mBottomBuffer = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, - UNSCALED_BOTTOM_BUFFER, displayMetrics); - mSelectedDateVerticalBarWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, - UNSCALED_SELECTED_DATE_VERTICAL_BAR_WIDTH, displayMetrics); - mWeekSeperatorLineWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, - UNSCALED_WEEK_SEPARATOR_LINE_WIDTH, displayMetrics); - - LayoutInflater layoutInflater = (LayoutInflater) context - .getSystemService(Service.LAYOUT_INFLATER_SERVICE); - View content = layoutInflater.inflate(R.layout.calendar_view, null, false); - addView(content); - - mListView = (ListView) findViewById(R.id.list); - mDayNamesHeader = (ViewGroup) content.findViewById(com.android.internal.R.id.day_names); - mMonthName = (TextView) content.findViewById(com.android.internal.R.id.month_name); - - setUpHeader(); - setUpListView(); - setUpAdapter(); - - // go to today or whichever is close to today min or max date - mTempDate.setTimeInMillis(System.currentTimeMillis()); - if (mTempDate.before(mMinDate)) { - goTo(mMinDate, false, true, true); - } else if (mMaxDate.before(mTempDate)) { - goTo(mMaxDate, false, true, true); - } else { - goTo(mTempDate, false, true, true); - } + public CalendarView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } - invalidate(); + public CalendarView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + mDelegate = new LegacyCalendarViewDelegate(this, context, attrs, defStyleAttr, defStyleRes); } /** @@ -425,10 +124,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_shownWeekCount */ public void setShownWeekCount(int count) { - if (mShownWeekCount != count) { - mShownWeekCount = count; - invalidate(); - } + mDelegate.setShownWeekCount(count); } /** @@ -439,7 +135,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_shownWeekCount */ public int getShownWeekCount() { - return mShownWeekCount; + return mDelegate.getShownWeekCount(); } /** @@ -450,16 +146,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_selectedWeekBackgroundColor */ public void setSelectedWeekBackgroundColor(int color) { - if (mSelectedWeekBackgroundColor != color) { - mSelectedWeekBackgroundColor = color; - final int childCount = mListView.getChildCount(); - for (int i = 0; i < childCount; i++) { - WeekView weekView = (WeekView) mListView.getChildAt(i); - if (weekView.mHasSelectedDay) { - weekView.invalidate(); - } - } - } + mDelegate.setSelectedWeekBackgroundColor(color); } /** @@ -470,7 +157,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_selectedWeekBackgroundColor */ public int getSelectedWeekBackgroundColor() { - return mSelectedWeekBackgroundColor; + return mDelegate.getSelectedWeekBackgroundColor(); } /** @@ -481,16 +168,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_focusedMonthDateColor */ public void setFocusedMonthDateColor(int color) { - if (mFocusedMonthDateColor != color) { - mFocusedMonthDateColor = color; - final int childCount = mListView.getChildCount(); - for (int i = 0; i < childCount; i++) { - WeekView weekView = (WeekView) mListView.getChildAt(i); - if (weekView.mHasFocusedDay) { - weekView.invalidate(); - } - } - } + mDelegate.setFocusedMonthDateColor(color); } /** @@ -501,7 +179,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_focusedMonthDateColor */ public int getFocusedMonthDateColor() { - return mFocusedMonthDateColor; + return mDelegate.getFocusedMonthDateColor(); } /** @@ -512,16 +190,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_unfocusedMonthDateColor */ public void setUnfocusedMonthDateColor(int color) { - if (mUnfocusedMonthDateColor != color) { - mUnfocusedMonthDateColor = color; - final int childCount = mListView.getChildCount(); - for (int i = 0; i < childCount; i++) { - WeekView weekView = (WeekView) mListView.getChildAt(i); - if (weekView.mHasUnfocusedDay) { - weekView.invalidate(); - } - } - } + mDelegate.setUnfocusedMonthDateColor(color); } /** @@ -532,7 +201,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_unfocusedMonthDateColor */ public int getUnfocusedMonthDateColor() { - return mFocusedMonthDateColor; + return mDelegate.getUnfocusedMonthDateColor(); } /** @@ -543,12 +212,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_weekNumberColor */ public void setWeekNumberColor(int color) { - if (mWeekNumberColor != color) { - mWeekNumberColor = color; - if (mShowWeekNumber) { - invalidateAllWeekViews(); - } - } + mDelegate.setWeekNumberColor(color); } /** @@ -559,7 +223,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_weekNumberColor */ public int getWeekNumberColor() { - return mWeekNumberColor; + return mDelegate.getWeekNumberColor(); } /** @@ -570,10 +234,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_weekSeparatorLineColor */ public void setWeekSeparatorLineColor(int color) { - if (mWeekSeparatorLineColor != color) { - mWeekSeparatorLineColor = color; - invalidateAllWeekViews(); - } + mDelegate.setWeekSeparatorLineColor(color); } /** @@ -584,7 +245,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_weekSeparatorLineColor */ public int getWeekSeparatorLineColor() { - return mWeekSeparatorLineColor; + return mDelegate.getWeekSeparatorLineColor(); } /** @@ -596,8 +257,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_selectedDateVerticalBar */ public void setSelectedDateVerticalBar(int resourceId) { - Drawable drawable = getResources().getDrawable(resourceId); - setSelectedDateVerticalBar(drawable); + mDelegate.setSelectedDateVerticalBar(resourceId); } /** @@ -609,16 +269,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_selectedDateVerticalBar */ public void setSelectedDateVerticalBar(Drawable drawable) { - if (mSelectedDateVerticalBar != drawable) { - mSelectedDateVerticalBar = drawable; - final int childCount = mListView.getChildCount(); - for (int i = 0; i < childCount; i++) { - WeekView weekView = (WeekView) mListView.getChildAt(i); - if (weekView.mHasSelectedDay) { - weekView.invalidate(); - } - } - } + mDelegate.setSelectedDateVerticalBar(drawable); } /** @@ -628,7 +279,7 @@ public class CalendarView extends FrameLayout { * @return The vertical bar drawable. */ public Drawable getSelectedDateVerticalBar() { - return mSelectedDateVerticalBar; + return mDelegate.getSelectedDateVerticalBar(); } /** @@ -639,10 +290,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_weekDayTextAppearance */ public void setWeekDayTextAppearance(int resourceId) { - if (mWeekDayTextAppearanceResId != resourceId) { - mWeekDayTextAppearanceResId = resourceId; - setUpHeader(); - } + mDelegate.setWeekDayTextAppearance(resourceId); } /** @@ -653,7 +301,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_weekDayTextAppearance */ public int getWeekDayTextAppearance() { - return mWeekDayTextAppearanceResId; + return mDelegate.getWeekDayTextAppearance(); } /** @@ -664,11 +312,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_dateTextAppearance */ public void setDateTextAppearance(int resourceId) { - if (mDateTextAppearanceResId != resourceId) { - mDateTextAppearanceResId = resourceId; - updateDateTextSize(); - invalidateAllWeekViews(); - } + mDelegate.setDateTextAppearance(resourceId); } /** @@ -679,35 +323,17 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_dateTextAppearance */ public int getDateTextAppearance() { - return mDateTextAppearanceResId; + return mDelegate.getDateTextAppearance(); } @Override public void setEnabled(boolean enabled) { - mListView.setEnabled(enabled); + mDelegate.setEnabled(enabled); } @Override public boolean isEnabled() { - return mListView.isEnabled(); - } - - @Override - protected void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - setCurrentLocale(newConfig.locale); - } - - @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(CalendarView.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(CalendarView.class.getName()); + return mDelegate.isEnabled(); } /** @@ -723,7 +349,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_minDate */ public long getMinDate() { - return mMinDate.getTimeInMillis(); + return mDelegate.getMinDate(); } /** @@ -736,30 +362,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_minDate */ public void setMinDate(long minDate) { - mTempDate.setTimeInMillis(minDate); - if (isSameDate(mTempDate, mMinDate)) { - return; - } - mMinDate.setTimeInMillis(minDate); - // make sure the current date is not earlier than - // the new min date since the latter is used for - // calculating the indices in the adapter thus - // avoiding out of bounds error - Calendar date = mAdapter.mSelectedDate; - if (date.before(mMinDate)) { - mAdapter.setSelectedDay(mMinDate); - } - // reinitialize the adapter since its range depends on min date - mAdapter.init(); - if (date.before(mMinDate)) { - setDate(mTempDate.getTimeInMillis()); - } else { - // we go to the current date to force the ListView to query its - // adapter for the shown views since we have changed the adapter - // range and the base from which the later calculates item indices - // note that calling setDate will not work since the date is the same - goTo(date, false, true, false); - } + mDelegate.setMinDate(minDate); } /** @@ -775,7 +378,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_maxDate */ public long getMaxDate() { - return mMaxDate.getTimeInMillis(); + return mDelegate.getMaxDate(); } /** @@ -788,23 +391,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_maxDate */ public void setMaxDate(long maxDate) { - mTempDate.setTimeInMillis(maxDate); - if (isSameDate(mTempDate, mMaxDate)) { - return; - } - mMaxDate.setTimeInMillis(maxDate); - // reinitialize the adapter since its range depends on max date - mAdapter.init(); - Calendar date = mAdapter.mSelectedDate; - if (date.after(mMaxDate)) { - setDate(mMaxDate.getTimeInMillis()); - } else { - // we go to the current date to force the ListView to query its - // adapter for the shown views since we have changed the adapter - // range and the base from which the later calculates item indices - // note that calling setDate will not work since the date is the same - goTo(date, false, true, false); - } + mDelegate.setMaxDate(maxDate); } /** @@ -815,12 +402,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_showWeekNumber */ public void setShowWeekNumber(boolean showWeekNumber) { - if (mShowWeekNumber == showWeekNumber) { - return; - } - mShowWeekNumber = showWeekNumber; - mAdapter.notifyDataSetChanged(); - setUpHeader(); + mDelegate.setShowWeekNumber(showWeekNumber); } /** @@ -831,7 +413,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_showWeekNumber */ public boolean getShowWeekNumber() { - return mShowWeekNumber; + return mDelegate.getShowWeekNumber(); } /** @@ -850,7 +432,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_firstDayOfWeek */ public int getFirstDayOfWeek() { - return mFirstDayOfWeek; + return mDelegate.getFirstDayOfWeek(); } /** @@ -869,12 +451,7 @@ public class CalendarView extends FrameLayout { * @attr ref android.R.styleable#CalendarView_firstDayOfWeek */ public void setFirstDayOfWeek(int firstDayOfWeek) { - if (mFirstDayOfWeek == firstDayOfWeek) { - return; - } - mFirstDayOfWeek = firstDayOfWeek; - mAdapter.init(); - setUpHeader(); + mDelegate.setFirstDayOfWeek(firstDayOfWeek); } /** @@ -883,7 +460,7 @@ public class CalendarView extends FrameLayout { * @param listener The listener to be notified. */ public void setOnDateChangeListener(OnDateChangeListener listener) { - mOnDateChangeListener = listener; + mDelegate.setOnDateChangeListener(listener); } /** @@ -893,7 +470,7 @@ public class CalendarView extends FrameLayout { * @return The selected date. */ public long getDate() { - return mAdapter.mSelectedDate.getTimeInMillis(); + return mDelegate.getDate(); } /** @@ -910,7 +487,7 @@ public class CalendarView extends FrameLayout { * @see #setMaxDate(long) */ public void setDate(long date) { - setDate(date, false, false); + mDelegate.setDate(date); } /** @@ -928,937 +505,1648 @@ public class CalendarView extends FrameLayout { * @see #setMaxDate(long) */ public void setDate(long date, boolean animate, boolean center) { - mTempDate.setTimeInMillis(date); - if (isSameDate(mTempDate, mAdapter.mSelectedDate)) { - return; - } - goTo(mTempDate, animate, true, center); + mDelegate.setDate(date, animate, center); } - private void updateDateTextSize() { - TypedArray dateTextAppearance = mContext.obtainStyledAttributes( - mDateTextAppearanceResId, R.styleable.TextAppearance); - mDateTextSize = dateTextAppearance.getDimensionPixelSize( - R.styleable.TextAppearance_textSize, DEFAULT_DATE_TEXT_SIZE); - dateTextAppearance.recycle(); + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + mDelegate.onConfigurationChanged(newConfig); } - /** - * Invalidates all week views. - */ - private void invalidateAllWeekViews() { - final int childCount = mListView.getChildCount(); - for (int i = 0; i < childCount; i++) { - View view = mListView.getChildAt(i); - view.invalidate(); - } + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + mDelegate.onInitializeAccessibilityEvent(event); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + mDelegate.onInitializeAccessibilityNodeInfo(info); } /** - * Sets the current locale. - * - * @param locale The current locale. + * A delegate interface that defined the public API of the CalendarView. Allows different + * CalendarView implementations. This would need to be implemented by the CalendarView delegates + * for the real behavior. */ - private void setCurrentLocale(Locale locale) { - if (locale.equals(mCurrentLocale)) { - return; - } + private interface CalendarViewDelegate { + void setShownWeekCount(int count); + int getShownWeekCount(); - mCurrentLocale = locale; + void setSelectedWeekBackgroundColor(int color); + int getSelectedWeekBackgroundColor(); - mTempDate = getCalendarForLocale(mTempDate, locale); - mFirstDayOfMonth = getCalendarForLocale(mFirstDayOfMonth, locale); - mMinDate = getCalendarForLocale(mMinDate, locale); - mMaxDate = getCalendarForLocale(mMaxDate, locale); - } + void setFocusedMonthDateColor(int color); + int getFocusedMonthDateColor(); - /** - * Gets a calendar for locale bootstrapped with the value of a given calendar. - * - * @param oldCalendar The old calendar. - * @param locale The locale. - */ - private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) { - if (oldCalendar == null) { - return Calendar.getInstance(locale); - } else { - final long currentTimeMillis = oldCalendar.getTimeInMillis(); - Calendar newCalendar = Calendar.getInstance(locale); - newCalendar.setTimeInMillis(currentTimeMillis); - return newCalendar; - } - } + void setUnfocusedMonthDateColor(int color); + int getUnfocusedMonthDateColor(); - /** - * @return True if the <code>firstDate</code> is the same as the <code> - * secondDate</code>. - */ - private boolean isSameDate(Calendar firstDate, Calendar secondDate) { - return (firstDate.get(Calendar.DAY_OF_YEAR) == secondDate.get(Calendar.DAY_OF_YEAR) - && firstDate.get(Calendar.YEAR) == secondDate.get(Calendar.YEAR)); - } + void setWeekNumberColor(int color); + int getWeekNumberColor(); - /** - * Creates a new adapter if necessary and sets up its parameters. - */ - private void setUpAdapter() { - if (mAdapter == null) { - mAdapter = new WeeksAdapter(); - mAdapter.registerDataSetObserver(new DataSetObserver() { - @Override - public void onChanged() { - if (mOnDateChangeListener != null) { - Calendar selectedDay = mAdapter.getSelectedDay(); - mOnDateChangeListener.onSelectedDayChange(CalendarView.this, - selectedDay.get(Calendar.YEAR), - selectedDay.get(Calendar.MONTH), - selectedDay.get(Calendar.DAY_OF_MONTH)); - } - } - }); - mListView.setAdapter(mAdapter); - } + void setWeekSeparatorLineColor(int color); + int getWeekSeparatorLineColor(); - // refresh the view with the new parameters - mAdapter.notifyDataSetChanged(); + void setSelectedDateVerticalBar(int resourceId); + void setSelectedDateVerticalBar(Drawable drawable); + Drawable getSelectedDateVerticalBar(); + + void setWeekDayTextAppearance(int resourceId); + int getWeekDayTextAppearance(); + + void setDateTextAppearance(int resourceId); + int getDateTextAppearance(); + + void setEnabled(boolean enabled); + boolean isEnabled(); + + void setMinDate(long minDate); + long getMinDate(); + + void setMaxDate(long maxDate); + long getMaxDate(); + + void setShowWeekNumber(boolean showWeekNumber); + boolean getShowWeekNumber(); + + void setFirstDayOfWeek(int firstDayOfWeek); + int getFirstDayOfWeek(); + + void setDate(long date); + void setDate(long date, boolean animate, boolean center); + long getDate(); + + void setOnDateChangeListener(OnDateChangeListener listener); + + void onConfigurationChanged(Configuration newConfig); + void onInitializeAccessibilityEvent(AccessibilityEvent event); + void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info); } /** - * Sets up the strings to be used by the header. + * An abstract class which can be used as a start for CalendarView implementations */ - private void setUpHeader() { - final String[] tinyWeekdayNames = LocaleData.get(Locale.getDefault()).tinyWeekdayNames; - mDayLabels = new String[mDaysPerWeek]; - for (int i = 0; i < mDaysPerWeek; i++) { - final int j = i + mFirstDayOfWeek; - final int calendarDay = (j > Calendar.SATURDAY) ? j - Calendar.SATURDAY : j; - mDayLabels[i] = tinyWeekdayNames[calendarDay]; - } - // Deal with week number - TextView label = (TextView) mDayNamesHeader.getChildAt(0); - if (mShowWeekNumber) { - label.setVisibility(View.VISIBLE); - } else { - label.setVisibility(View.GONE); + abstract static class AbstractCalendarViewDelegate implements CalendarViewDelegate { + // The delegator + protected CalendarView mDelegator; + + // The context + protected Context mContext; + + // The current locale + protected Locale mCurrentLocale; + + AbstractCalendarViewDelegate(CalendarView delegator, Context context) { + mDelegator = delegator; + mContext = context; + + // Initialization based on locale + setCurrentLocale(Locale.getDefault()); } - // Deal with day labels - final int count = mDayNamesHeader.getChildCount(); - for (int i = 0; i < count - 1; i++) { - label = (TextView) mDayNamesHeader.getChildAt(i + 1); - if (mWeekDayTextAppearanceResId > -1) { - label.setTextAppearance(mContext, mWeekDayTextAppearanceResId); - } - if (i < mDaysPerWeek) { - label.setText(mDayLabels[i]); - label.setVisibility(View.VISIBLE); - } else { - label.setVisibility(View.GONE); + + protected void setCurrentLocale(Locale locale) { + if (locale.equals(mCurrentLocale)) { + return; } + mCurrentLocale = locale; } - mDayNamesHeader.invalidate(); } /** - * Sets all the required fields for the list view. + * A delegate implementing the legacy CalendarView */ - private void setUpListView() { - // Configure the listview - mListView.setDivider(null); - mListView.setItemsCanFocus(true); - mListView.setVerticalScrollBarEnabled(false); - mListView.setOnScrollListener(new OnScrollListener() { - public void onScrollStateChanged(AbsListView view, int scrollState) { - CalendarView.this.onScrollStateChanged(view, scrollState); - } - - public void onScroll( - AbsListView view, int firstVisibleItem, int visibleItemCount, - int totalItemCount) { - CalendarView.this.onScroll(view, firstVisibleItem, visibleItemCount, - totalItemCount); - } - }); - // Make the scrolling behavior nicer - mListView.setFriction(mFriction); - mListView.setVelocityScale(mVelocityScale); - } + private static class LegacyCalendarViewDelegate extends AbstractCalendarViewDelegate { - /** - * This moves to the specified time in the view. If the time is not already - * in range it will move the list so that the first of the month containing - * the time is at the top of the view. If the new time is already in view - * the list will not be scrolled unless forceScroll is true. This time may - * optionally be highlighted as selected as well. - * - * @param date The time to move to. - * @param animate Whether to scroll to the given time or just redraw at the - * new location. - * @param setSelected Whether to set the given time as selected. - * @param forceScroll Whether to recenter even if the time is already - * visible. - * - * @throws IllegalArgumentException of the provided date is before the - * range start of after the range end. - */ - private void goTo(Calendar date, boolean animate, boolean setSelected, boolean forceScroll) { - if (date.before(mMinDate) || date.after(mMaxDate)) { - throw new IllegalArgumentException("Time not between " + mMinDate.getTime() - + " and " + mMaxDate.getTime()); - } - // Find the first and last entirely visible weeks - int firstFullyVisiblePosition = mListView.getFirstVisiblePosition(); - View firstChild = mListView.getChildAt(0); - if (firstChild != null && firstChild.getTop() < 0) { - firstFullyVisiblePosition++; - } - int lastFullyVisiblePosition = firstFullyVisiblePosition + mShownWeekCount - 1; - if (firstChild != null && firstChild.getTop() > mBottomBuffer) { - lastFullyVisiblePosition--; - } - if (setSelected) { - mAdapter.setSelectedDay(date); - } - // Get the week we're going to - int position = getWeeksSinceMinDate(date); + /** + * Default value whether to show week number. + */ + private static final boolean DEFAULT_SHOW_WEEK_NUMBER = true; - // Check if the selected day is now outside of our visible range - // and if so scroll to the month that contains it - if (position < firstFullyVisiblePosition || position > lastFullyVisiblePosition - || forceScroll) { - mFirstDayOfMonth.setTimeInMillis(date.getTimeInMillis()); - mFirstDayOfMonth.set(Calendar.DAY_OF_MONTH, 1); + /** + * The number of milliseconds in a day.e + */ + private static final long MILLIS_IN_DAY = 86400000L; - setMonthDisplayed(mFirstDayOfMonth); + /** + * The number of day in a week. + */ + private static final int DAYS_PER_WEEK = 7; - // the earliest time we can scroll to is the min date - if (mFirstDayOfMonth.before(mMinDate)) { - position = 0; - } else { - position = getWeeksSinceMinDate(mFirstDayOfMonth); - } + /** + * The number of milliseconds in a week. + */ + private static final long MILLIS_IN_WEEK = DAYS_PER_WEEK * MILLIS_IN_DAY; - mPreviousScrollState = OnScrollListener.SCROLL_STATE_FLING; - if (animate) { - mListView.smoothScrollToPositionFromTop(position, mListScrollTopOffset, - GOTO_SCROLL_DURATION); - } else { - mListView.setSelectionFromTop(position, mListScrollTopOffset); - // Perform any after scroll operations that are needed - onScrollStateChanged(mListView, OnScrollListener.SCROLL_STATE_IDLE); - } - } else if (setSelected) { - // Otherwise just set the selection - setMonthDisplayed(date); - } - } + /** + * Affects when the month selection will change while scrolling upe + */ + private static final int SCROLL_HYST_WEEKS = 2; - /** - * Parses the given <code>date</code> and in case of success sets - * the result to the <code>outDate</code>. - * - * @return True if the date was parsed. - */ - private boolean parseDate(String date, Calendar outDate) { - try { - outDate.setTime(mDateFormat.parse(date)); - return true; - } catch (ParseException e) { - Log.w(LOG_TAG, "Date: " + date + " not in format: " + DATE_FORMAT); - return false; - } - } + /** + * How long the GoTo fling animation should last. + */ + private static final int GOTO_SCROLL_DURATION = 1000; - /** - * Called when a <code>view</code> transitions to a new <code>scrollState - * </code>. - */ - private void onScrollStateChanged(AbsListView view, int scrollState) { - mScrollStateChangedRunnable.doScrollStateChange(view, scrollState); - } + /** + * The duration of the adjustment upon a user scroll in milliseconds. + */ + private static final int ADJUSTMENT_SCROLL_DURATION = 500; - /** - * Updates the title and selected month if the <code>view</code> has moved to a new - * month. - */ - private void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, - int totalItemCount) { - WeekView child = (WeekView) view.getChildAt(0); - if (child == null) { - return; - } + /** + * How long to wait after receiving an onScrollStateChanged notification + * before acting on it. + */ + private static final int SCROLL_CHANGE_DELAY = 40; - // Figure out where we are - long currScroll = view.getFirstVisiblePosition() * child.getHeight() - child.getBottom(); + /** + * String for parsing dates. + */ + private static final String DATE_FORMAT = "MM/dd/yyyy"; - // If we have moved since our last call update the direction - if (currScroll < mPreviousScrollPosition) { - mIsScrollingUp = true; - } else if (currScroll > mPreviousScrollPosition) { - mIsScrollingUp = false; - } else { - return; - } + /** + * The default minimal date. + */ + private static final String DEFAULT_MIN_DATE = "01/01/1900"; - // Use some hysteresis for checking which month to highlight. This - // causes the month to transition when two full weeks of a month are - // visible when scrolling up, and when the first day in a month reaches - // the top of the screen when scrolling down. - int offset = child.getBottom() < mWeekMinVisibleHeight ? 1 : 0; - if (mIsScrollingUp) { - child = (WeekView) view.getChildAt(SCROLL_HYST_WEEKS + offset); - } else if (offset != 0) { - child = (WeekView) view.getChildAt(offset); - } + /** + * The default maximal date. + */ + private static final String DEFAULT_MAX_DATE = "01/01/2100"; - if (child != null) { - // Find out which month we're moving into - int month; - if (mIsScrollingUp) { - month = child.getMonthOfFirstWeekDay(); - } else { - month = child.getMonthOfLastWeekDay(); - } + private static final int DEFAULT_SHOWN_WEEK_COUNT = 6; - // And how it relates to our current highlighted month - int monthDiff; - if (mCurrentMonthDisplayed == 11 && month == 0) { - monthDiff = 1; - } else if (mCurrentMonthDisplayed == 0 && month == 11) { - monthDiff = -1; - } else { - monthDiff = month - mCurrentMonthDisplayed; - } + private static final int DEFAULT_DATE_TEXT_SIZE = 14; - // Only switch months if we're scrolling away from the currently - // selected month - if ((!mIsScrollingUp && monthDiff > 0) || (mIsScrollingUp && monthDiff < 0)) { - Calendar firstDay = child.getFirstDay(); - if (mIsScrollingUp) { - firstDay.add(Calendar.DAY_OF_MONTH, -DAYS_PER_WEEK); - } else { - firstDay.add(Calendar.DAY_OF_MONTH, DAYS_PER_WEEK); - } - setMonthDisplayed(firstDay); - } - } + private static final int UNSCALED_SELECTED_DATE_VERTICAL_BAR_WIDTH = 6; - mPreviousScrollPosition = currScroll; - mPreviousScrollState = mCurrentScrollState; - } + private static final int UNSCALED_WEEK_MIN_VISIBLE_HEIGHT = 12; - /** - * Sets the month displayed at the top of this view based on time. Override - * to add custom events when the title is changed. - * - * @param calendar A day in the new focus month. - */ - private void setMonthDisplayed(Calendar calendar) { - mCurrentMonthDisplayed = calendar.get(Calendar.MONTH); - mAdapter.setFocusMonth(mCurrentMonthDisplayed); - final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_MONTH_DAY - | DateUtils.FORMAT_SHOW_YEAR; - final long millis = calendar.getTimeInMillis(); - String newMonthName = DateUtils.formatDateRange(mContext, millis, millis, flags); - mMonthName.setText(newMonthName); - mMonthName.invalidate(); - } + private static final int UNSCALED_LIST_SCROLL_TOP_OFFSET = 2; - /** - * @return Returns the number of weeks between the current <code>date</code> - * and the <code>mMinDate</code>. - */ - private int getWeeksSinceMinDate(Calendar date) { - if (date.before(mMinDate)) { - throw new IllegalArgumentException("fromDate: " + mMinDate.getTime() - + " does not precede toDate: " + date.getTime()); - } - long endTimeMillis = date.getTimeInMillis() - + date.getTimeZone().getOffset(date.getTimeInMillis()); - long startTimeMillis = mMinDate.getTimeInMillis() - + mMinDate.getTimeZone().getOffset(mMinDate.getTimeInMillis()); - long dayOffsetMillis = (mMinDate.get(Calendar.DAY_OF_WEEK) - mFirstDayOfWeek) - * MILLIS_IN_DAY; - return (int) ((endTimeMillis - startTimeMillis + dayOffsetMillis) / MILLIS_IN_WEEK); - } + private static final int UNSCALED_BOTTOM_BUFFER = 20; - /** - * Command responsible for acting upon scroll state changes. - */ - private class ScrollStateRunnable implements Runnable { - private AbsListView mView; + private static final int UNSCALED_WEEK_SEPARATOR_LINE_WIDTH = 1; + + private static final int DEFAULT_WEEK_DAY_TEXT_APPEARANCE_RES_ID = -1; + + private final int mWeekSeperatorLineWidth; + + private int mDateTextSize; + + private Drawable mSelectedDateVerticalBar; + + private final int mSelectedDateVerticalBarWidth; + + private int mSelectedWeekBackgroundColor; + + private int mFocusedMonthDateColor; - private int mNewState; + private int mUnfocusedMonthDateColor; + + private int mWeekSeparatorLineColor; + + private int mWeekNumberColor; + + private int mWeekDayTextAppearanceResId; + + private int mDateTextAppearanceResId; /** - * Sets up the runnable with a short delay in case the scroll state - * immediately changes again. - * - * @param view The list view that changed state - * @param scrollState The new state it changed to + * The top offset of the weeks list. */ - public void doScrollStateChange(AbsListView view, int scrollState) { - mView = view; - mNewState = scrollState; - removeCallbacks(this); - postDelayed(this, SCROLL_CHANGE_DELAY); - } + private int mListScrollTopOffset = 2; - public void run() { - mCurrentScrollState = mNewState; - // Fix the position after a scroll or a fling ends - if (mNewState == OnScrollListener.SCROLL_STATE_IDLE - && mPreviousScrollState != OnScrollListener.SCROLL_STATE_IDLE) { - View child = mView.getChildAt(0); - if (child == null) { - // The view is no longer visible, just return - return; - } - int dist = child.getBottom() - mListScrollTopOffset; - if (dist > mListScrollTopOffset) { - if (mIsScrollingUp) { - mView.smoothScrollBy(dist - child.getHeight(), ADJUSTMENT_SCROLL_DURATION); - } else { - mView.smoothScrollBy(dist, ADJUSTMENT_SCROLL_DURATION); - } - } - } - mPreviousScrollState = mNewState; - } - } + /** + * The visible height of a week view. + */ + private int mWeekMinVisibleHeight = 12; - /** - * <p> - * This is a specialized adapter for creating a list of weeks with - * selectable days. It can be configured to display the week number, start - * the week on a given day, show a reduced number of days, or display an - * arbitrary number of weeks at a time. - * </p> - */ - private class WeeksAdapter extends BaseAdapter implements OnTouchListener { - private final Calendar mSelectedDate = Calendar.getInstance(); - private final GestureDetector mGestureDetector; + /** + * The visible height of a week view. + */ + private int mBottomBuffer = 20; - private int mSelectedWeek; + /** + * The number of shown weeks. + */ + private int mShownWeekCount; - private int mFocusedMonth; + /** + * Flag whether to show the week number. + */ + private boolean mShowWeekNumber; - private int mTotalWeekCount; + /** + * The number of day per week to be shown. + */ + private int mDaysPerWeek = 7; - public WeeksAdapter() { - mGestureDetector = new GestureDetector(mContext, new CalendarGestureListener()); - init(); - } + /** + * The friction of the week list while flinging. + */ + private float mFriction = .05f; /** - * Set up the gesture detector and selected time + * Scale for adjusting velocity of the week list while flinging. */ - private void init() { - mSelectedWeek = getWeeksSinceMinDate(mSelectedDate); - mTotalWeekCount = getWeeksSinceMinDate(mMaxDate); - if (mMinDate.get(Calendar.DAY_OF_WEEK) != mFirstDayOfWeek - || mMaxDate.get(Calendar.DAY_OF_WEEK) != mFirstDayOfWeek) { - mTotalWeekCount++; - } - notifyDataSetChanged(); - } + private float mVelocityScale = 0.333f; /** - * Updates the selected day and related parameters. - * - * @param selectedDay The time to highlight + * The adapter for the weeks list. */ - public void setSelectedDay(Calendar selectedDay) { - if (selectedDay.get(Calendar.DAY_OF_YEAR) == mSelectedDate.get(Calendar.DAY_OF_YEAR) - && selectedDay.get(Calendar.YEAR) == mSelectedDate.get(Calendar.YEAR)) { - return; - } - mSelectedDate.setTimeInMillis(selectedDay.getTimeInMillis()); - mSelectedWeek = getWeeksSinceMinDate(mSelectedDate); - mFocusedMonth = mSelectedDate.get(Calendar.MONTH); - notifyDataSetChanged(); - } + private WeeksAdapter mAdapter; /** - * @return The selected day of month. + * The weeks list. */ - public Calendar getSelectedDay() { - return mSelectedDate; + private ListView mListView; + + /** + * The name of the month to display. + */ + private TextView mMonthName; + + /** + * The header with week day names. + */ + private ViewGroup mDayNamesHeader; + + /** + * Cached labels for the week names header. + */ + private String[] mDayLabels; + + /** + * The first day of the week. + */ + private int mFirstDayOfWeek; + + /** + * Which month should be displayed/highlighted [0-11]. + */ + private int mCurrentMonthDisplayed = -1; + + /** + * Used for tracking during a scroll. + */ + private long mPreviousScrollPosition; + + /** + * Used for tracking which direction the view is scrolling. + */ + private boolean mIsScrollingUp = false; + + /** + * The previous scroll state of the weeks ListView. + */ + private int mPreviousScrollState = OnScrollListener.SCROLL_STATE_IDLE; + + /** + * The current scroll state of the weeks ListView. + */ + private int mCurrentScrollState = OnScrollListener.SCROLL_STATE_IDLE; + + /** + * Listener for changes in the selected day. + */ + private OnDateChangeListener mOnDateChangeListener; + + /** + * Command for adjusting the position after a scroll/fling. + */ + private ScrollStateRunnable mScrollStateChangedRunnable = new ScrollStateRunnable(); + + /** + * Temporary instance to avoid multiple instantiations. + */ + private Calendar mTempDate; + + /** + * The first day of the focused month. + */ + private Calendar mFirstDayOfMonth; + + /** + * The start date of the range supported by this picker. + */ + private Calendar mMinDate; + + /** + * The end date of the range supported by this picker. + */ + private Calendar mMaxDate; + + /** + * Date format for parsing dates. + */ + private final java.text.DateFormat mDateFormat = new SimpleDateFormat(DATE_FORMAT); + + LegacyCalendarViewDelegate(CalendarView delegator, Context context, AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + super(delegator, context); + + // initialization based on locale + setCurrentLocale(Locale.getDefault()); + + TypedArray attributesArray = context.obtainStyledAttributes(attrs, + R.styleable.CalendarView, defStyleAttr, defStyleRes); + mShowWeekNumber = attributesArray.getBoolean(R.styleable.CalendarView_showWeekNumber, + DEFAULT_SHOW_WEEK_NUMBER); + mFirstDayOfWeek = attributesArray.getInt(R.styleable.CalendarView_firstDayOfWeek, + LocaleData.get(Locale.getDefault()).firstDayOfWeek); + String minDate = attributesArray.getString(R.styleable.CalendarView_minDate); + if (TextUtils.isEmpty(minDate) || !parseDate(minDate, mMinDate)) { + parseDate(DEFAULT_MIN_DATE, mMinDate); + } + String maxDate = attributesArray.getString(R.styleable.CalendarView_maxDate); + if (TextUtils.isEmpty(maxDate) || !parseDate(maxDate, mMaxDate)) { + parseDate(DEFAULT_MAX_DATE, mMaxDate); + } + if (mMaxDate.before(mMinDate)) { + throw new IllegalArgumentException("Max date cannot be before min date."); + } + mShownWeekCount = attributesArray.getInt(R.styleable.CalendarView_shownWeekCount, + DEFAULT_SHOWN_WEEK_COUNT); + mSelectedWeekBackgroundColor = attributesArray.getColor( + R.styleable.CalendarView_selectedWeekBackgroundColor, 0); + mFocusedMonthDateColor = attributesArray.getColor( + R.styleable.CalendarView_focusedMonthDateColor, 0); + mUnfocusedMonthDateColor = attributesArray.getColor( + R.styleable.CalendarView_unfocusedMonthDateColor, 0); + mWeekSeparatorLineColor = attributesArray.getColor( + R.styleable.CalendarView_weekSeparatorLineColor, 0); + mWeekNumberColor = attributesArray.getColor(R.styleable.CalendarView_weekNumberColor, 0); + mSelectedDateVerticalBar = attributesArray.getDrawable( + R.styleable.CalendarView_selectedDateVerticalBar); + + mDateTextAppearanceResId = attributesArray.getResourceId( + R.styleable.CalendarView_dateTextAppearance, R.style.TextAppearance_Small); + updateDateTextSize(); + + mWeekDayTextAppearanceResId = attributesArray.getResourceId( + R.styleable.CalendarView_weekDayTextAppearance, + DEFAULT_WEEK_DAY_TEXT_APPEARANCE_RES_ID); + attributesArray.recycle(); + + DisplayMetrics displayMetrics = mDelegator.getResources().getDisplayMetrics(); + mWeekMinVisibleHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + UNSCALED_WEEK_MIN_VISIBLE_HEIGHT, displayMetrics); + mListScrollTopOffset = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + UNSCALED_LIST_SCROLL_TOP_OFFSET, displayMetrics); + mBottomBuffer = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + UNSCALED_BOTTOM_BUFFER, displayMetrics); + mSelectedDateVerticalBarWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + UNSCALED_SELECTED_DATE_VERTICAL_BAR_WIDTH, displayMetrics); + mWeekSeperatorLineWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + UNSCALED_WEEK_SEPARATOR_LINE_WIDTH, displayMetrics); + + LayoutInflater layoutInflater = (LayoutInflater) mContext + .getSystemService(Service.LAYOUT_INFLATER_SERVICE); + View content = layoutInflater.inflate(R.layout.calendar_view, null, false); + mDelegator.addView(content); + + mListView = (ListView) mDelegator.findViewById(R.id.list); + mDayNamesHeader = (ViewGroup) content.findViewById(com.android.internal.R.id.day_names); + mMonthName = (TextView) content.findViewById(com.android.internal.R.id.month_name); + + setUpHeader(); + setUpListView(); + setUpAdapter(); + + // go to today or whichever is close to today min or max date + mTempDate.setTimeInMillis(System.currentTimeMillis()); + if (mTempDate.before(mMinDate)) { + goTo(mMinDate, false, true, true); + } else if (mMaxDate.before(mTempDate)) { + goTo(mMaxDate, false, true, true); + } else { + goTo(mTempDate, false, true, true); + } + + mDelegator.invalidate(); } @Override - public int getCount() { - return mTotalWeekCount; + public void setShownWeekCount(int count) { + if (mShownWeekCount != count) { + mShownWeekCount = count; + mDelegator.invalidate(); + } } @Override - public Object getItem(int position) { - return null; + public int getShownWeekCount() { + return mShownWeekCount; } @Override - public long getItemId(int position) { - return position; + public void setSelectedWeekBackgroundColor(int color) { + if (mSelectedWeekBackgroundColor != color) { + mSelectedWeekBackgroundColor = color; + final int childCount = mListView.getChildCount(); + for (int i = 0; i < childCount; i++) { + WeekView weekView = (WeekView) mListView.getChildAt(i); + if (weekView.mHasSelectedDay) { + weekView.invalidate(); + } + } + } } @Override - public View getView(int position, View convertView, ViewGroup parent) { - WeekView weekView = null; - if (convertView != null) { - weekView = (WeekView) convertView; - } else { - weekView = new WeekView(mContext); - android.widget.AbsListView.LayoutParams params = - new android.widget.AbsListView.LayoutParams(LayoutParams.WRAP_CONTENT, - LayoutParams.WRAP_CONTENT); - weekView.setLayoutParams(params); - weekView.setClickable(true); - weekView.setOnTouchListener(this); - } + public int getSelectedWeekBackgroundColor() { + return mSelectedWeekBackgroundColor; + } - int selectedWeekDay = (mSelectedWeek == position) ? mSelectedDate.get( - Calendar.DAY_OF_WEEK) : -1; - weekView.init(position, selectedWeekDay, mFocusedMonth); + @Override + public void setFocusedMonthDateColor(int color) { + if (mFocusedMonthDateColor != color) { + mFocusedMonthDateColor = color; + final int childCount = mListView.getChildCount(); + for (int i = 0; i < childCount; i++) { + WeekView weekView = (WeekView) mListView.getChildAt(i); + if (weekView.mHasFocusedDay) { + weekView.invalidate(); + } + } + } + } - return weekView; + @Override + public int getFocusedMonthDateColor() { + return mFocusedMonthDateColor; } - /** - * Changes which month is in focus and updates the view. - * - * @param month The month to show as in focus [0-11] - */ - public void setFocusMonth(int month) { - if (mFocusedMonth == month) { - return; + @Override + public void setUnfocusedMonthDateColor(int color) { + if (mUnfocusedMonthDateColor != color) { + mUnfocusedMonthDateColor = color; + final int childCount = mListView.getChildCount(); + for (int i = 0; i < childCount; i++) { + WeekView weekView = (WeekView) mListView.getChildAt(i); + if (weekView.mHasUnfocusedDay) { + weekView.invalidate(); + } + } } - mFocusedMonth = month; - notifyDataSetChanged(); } @Override - public boolean onTouch(View v, MotionEvent event) { - if (mListView.isEnabled() && mGestureDetector.onTouchEvent(event)) { - WeekView weekView = (WeekView) v; - // if we cannot find a day for the given location we are done - if (!weekView.getDayFromLocation(event.getX(), mTempDate)) { - return true; - } - // it is possible that the touched day is outside the valid range - // we draw whole weeks but range end can fall not on the week end - if (mTempDate.before(mMinDate) || mTempDate.after(mMaxDate)) { - return true; + public int getUnfocusedMonthDateColor() { + return mFocusedMonthDateColor; + } + + @Override + public void setWeekNumberColor(int color) { + if (mWeekNumberColor != color) { + mWeekNumberColor = color; + if (mShowWeekNumber) { + invalidateAllWeekViews(); } - onDateTapped(mTempDate); - return true; } - return false; } - /** - * Maintains the same hour/min/sec but moves the day to the tapped day. - * - * @param day The day that was tapped - */ - private void onDateTapped(Calendar day) { - setSelectedDay(day); - setMonthDisplayed(day); + @Override + public int getWeekNumberColor() { + return mWeekNumberColor; } - /** - * This is here so we can identify single tap events and set the - * selected day correctly - */ - class CalendarGestureListener extends GestureDetector.SimpleOnGestureListener { - @Override - public boolean onSingleTapUp(MotionEvent e) { - return true; + @Override + public void setWeekSeparatorLineColor(int color) { + if (mWeekSeparatorLineColor != color) { + mWeekSeparatorLineColor = color; + invalidateAllWeekViews(); } } - } - /** - * <p> - * This is a dynamic view for drawing a single week. It can be configured to - * display the week number, start the week on a given day, or show a reduced - * number of days. It is intended for use as a single view within a - * ListView. See {@link WeeksAdapter} for usage. - * </p> - */ - private class WeekView extends View { + @Override + public int getWeekSeparatorLineColor() { + return mWeekSeparatorLineColor; + } - private final Rect mTempRect = new Rect(); + @Override + public void setSelectedDateVerticalBar(int resourceId) { + Drawable drawable = mDelegator.getContext().getDrawable(resourceId); + setSelectedDateVerticalBar(drawable); + } - private final Paint mDrawPaint = new Paint(); + @Override + public void setSelectedDateVerticalBar(Drawable drawable) { + if (mSelectedDateVerticalBar != drawable) { + mSelectedDateVerticalBar = drawable; + final int childCount = mListView.getChildCount(); + for (int i = 0; i < childCount; i++) { + WeekView weekView = (WeekView) mListView.getChildAt(i); + if (weekView.mHasSelectedDay) { + weekView.invalidate(); + } + } + } + } - private final Paint mMonthNumDrawPaint = new Paint(); + @Override + public Drawable getSelectedDateVerticalBar() { + return mSelectedDateVerticalBar; + } - // Cache the number strings so we don't have to recompute them each time - private String[] mDayNumbers; + @Override + public void setWeekDayTextAppearance(int resourceId) { + if (mWeekDayTextAppearanceResId != resourceId) { + mWeekDayTextAppearanceResId = resourceId; + setUpHeader(); + } + } + + @Override + public int getWeekDayTextAppearance() { + return mWeekDayTextAppearanceResId; + } + + @Override + public void setDateTextAppearance(int resourceId) { + if (mDateTextAppearanceResId != resourceId) { + mDateTextAppearanceResId = resourceId; + updateDateTextSize(); + invalidateAllWeekViews(); + } + } + + @Override + public int getDateTextAppearance() { + return mDateTextAppearanceResId; + } - // Quick lookup for checking which days are in the focus month - private boolean[] mFocusDay; + @Override + public void setEnabled(boolean enabled) { + mListView.setEnabled(enabled); + } + + @Override + public boolean isEnabled() { + return mListView.isEnabled(); + } - // Whether this view has a focused day. - private boolean mHasFocusedDay; + @Override + public void setMinDate(long minDate) { + mTempDate.setTimeInMillis(minDate); + if (isSameDate(mTempDate, mMinDate)) { + return; + } + mMinDate.setTimeInMillis(minDate); + // make sure the current date is not earlier than + // the new min date since the latter is used for + // calculating the indices in the adapter thus + // avoiding out of bounds error + Calendar date = mAdapter.mSelectedDate; + if (date.before(mMinDate)) { + mAdapter.setSelectedDay(mMinDate); + } + // reinitialize the adapter since its range depends on min date + mAdapter.init(); + if (date.before(mMinDate)) { + setDate(mTempDate.getTimeInMillis()); + } else { + // we go to the current date to force the ListView to query its + // adapter for the shown views since we have changed the adapter + // range and the base from which the later calculates item indices + // note that calling setDate will not work since the date is the same + goTo(date, false, true, false); + } + } - // Whether this view has only focused days. - private boolean mHasUnfocusedDay; + @Override + public long getMinDate() { + return mMinDate.getTimeInMillis(); + } - // The first day displayed by this item - private Calendar mFirstDay; + @Override + public void setMaxDate(long maxDate) { + mTempDate.setTimeInMillis(maxDate); + if (isSameDate(mTempDate, mMaxDate)) { + return; + } + mMaxDate.setTimeInMillis(maxDate); + // reinitialize the adapter since its range depends on max date + mAdapter.init(); + Calendar date = mAdapter.mSelectedDate; + if (date.after(mMaxDate)) { + setDate(mMaxDate.getTimeInMillis()); + } else { + // we go to the current date to force the ListView to query its + // adapter for the shown views since we have changed the adapter + // range and the base from which the later calculates item indices + // note that calling setDate will not work since the date is the same + goTo(date, false, true, false); + } + } - // The month of the first day in this week - private int mMonthOfFirstWeekDay = -1; + @Override + public long getMaxDate() { + return mMaxDate.getTimeInMillis(); + } - // The month of the last day in this week - private int mLastWeekDayMonth = -1; + @Override + public void setShowWeekNumber(boolean showWeekNumber) { + if (mShowWeekNumber == showWeekNumber) { + return; + } + mShowWeekNumber = showWeekNumber; + mAdapter.notifyDataSetChanged(); + setUpHeader(); + } - // The position of this week, equivalent to weeks since the week of Jan - // 1st, 1900 - private int mWeek = -1; + @Override + public boolean getShowWeekNumber() { + return mShowWeekNumber; + } - // Quick reference to the width of this view, matches parent - private int mWidth; + @Override + public void setFirstDayOfWeek(int firstDayOfWeek) { + if (mFirstDayOfWeek == firstDayOfWeek) { + return; + } + mFirstDayOfWeek = firstDayOfWeek; + mAdapter.init(); + mAdapter.notifyDataSetChanged(); + setUpHeader(); + } - // The height this view should draw at in pixels, set by height param - private int mHeight; + @Override + public int getFirstDayOfWeek() { + return mFirstDayOfWeek; + } - // If this view contains the selected day - private boolean mHasSelectedDay = false; + @Override + public void setDate(long date) { + setDate(date, false, false); + } - // Which day is selected [0-6] or -1 if no day is selected - private int mSelectedDay = -1; + @Override + public void setDate(long date, boolean animate, boolean center) { + mTempDate.setTimeInMillis(date); + if (isSameDate(mTempDate, mAdapter.mSelectedDate)) { + return; + } + goTo(mTempDate, animate, true, center); + } - // The number of days + a spot for week number if it is displayed - private int mNumCells; + @Override + public long getDate() { + return mAdapter.mSelectedDate.getTimeInMillis(); + } - // The left edge of the selected day - private int mSelectedLeft = -1; + @Override + public void setOnDateChangeListener(OnDateChangeListener listener) { + mOnDateChangeListener = listener; + } - // The right edge of the selected day - private int mSelectedRight = -1; + @Override + public void onConfigurationChanged(Configuration newConfig) { + setCurrentLocale(newConfig.locale); + } - public WeekView(Context context) { - super(context); + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + event.setClassName(CalendarView.class.getName()); + } - // Sets up any standard paints that will be used - initilaizePaints(); + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + info.setClassName(CalendarView.class.getName()); } /** - * Initializes this week view. + * Sets the current locale. * - * @param weekNumber The number of the week this view represents. The - * week number is a zero based index of the weeks since - * {@link CalendarView#getMinDate()}. - * @param selectedWeekDay The selected day of the week from 0 to 6, -1 if no - * selected day. - * @param focusedMonth The month that is currently in focus i.e. - * highlighted. + * @param locale The current locale. */ - public void init(int weekNumber, int selectedWeekDay, int focusedMonth) { - mSelectedDay = selectedWeekDay; - mHasSelectedDay = mSelectedDay != -1; - mNumCells = mShowWeekNumber ? mDaysPerWeek + 1 : mDaysPerWeek; - mWeek = weekNumber; - mTempDate.setTimeInMillis(mMinDate.getTimeInMillis()); - - mTempDate.add(Calendar.WEEK_OF_YEAR, mWeek); - mTempDate.setFirstDayOfWeek(mFirstDayOfWeek); - - // Allocate space for caching the day numbers and focus values - mDayNumbers = new String[mNumCells]; - mFocusDay = new boolean[mNumCells]; - - // If we're showing the week number calculate it based on Monday - int i = 0; - if (mShowWeekNumber) { - mDayNumbers[0] = String.format(Locale.getDefault(), "%d", - mTempDate.get(Calendar.WEEK_OF_YEAR)); - i++; - } - - // Now adjust our starting day based on the start day of the week - int diff = mFirstDayOfWeek - mTempDate.get(Calendar.DAY_OF_WEEK); - mTempDate.add(Calendar.DAY_OF_MONTH, diff); - - mFirstDay = (Calendar) mTempDate.clone(); - mMonthOfFirstWeekDay = mTempDate.get(Calendar.MONTH); - - mHasUnfocusedDay = true; - for (; i < mNumCells; i++) { - final boolean isFocusedDay = (mTempDate.get(Calendar.MONTH) == focusedMonth); - mFocusDay[i] = isFocusedDay; - mHasFocusedDay |= isFocusedDay; - mHasUnfocusedDay &= !isFocusedDay; - // do not draw dates outside the valid range to avoid user confusion - if (mTempDate.before(mMinDate) || mTempDate.after(mMaxDate)) { - mDayNumbers[i] = ""; - } else { - mDayNumbers[i] = String.format(Locale.getDefault(), "%d", - mTempDate.get(Calendar.DAY_OF_MONTH)); - } - mTempDate.add(Calendar.DAY_OF_MONTH, 1); - } - // We do one extra add at the end of the loop, if that pushed us to - // new month undo it - if (mTempDate.get(Calendar.DAY_OF_MONTH) == 1) { - mTempDate.add(Calendar.DAY_OF_MONTH, -1); - } - mLastWeekDayMonth = mTempDate.get(Calendar.MONTH); + @Override + protected void setCurrentLocale(Locale locale) { + super.setCurrentLocale(locale); - updateSelectionPositions(); + mTempDate = getCalendarForLocale(mTempDate, locale); + mFirstDayOfMonth = getCalendarForLocale(mFirstDayOfMonth, locale); + mMinDate = getCalendarForLocale(mMinDate, locale); + mMaxDate = getCalendarForLocale(mMaxDate, locale); + } + private void updateDateTextSize() { + TypedArray dateTextAppearance = mDelegator.getContext().obtainStyledAttributes( + mDateTextAppearanceResId, R.styleable.TextAppearance); + mDateTextSize = dateTextAppearance.getDimensionPixelSize( + R.styleable.TextAppearance_textSize, DEFAULT_DATE_TEXT_SIZE); + dateTextAppearance.recycle(); } /** - * Initialize the paint instances. + * Invalidates all week views. */ - private void initilaizePaints() { - mDrawPaint.setFakeBoldText(false); - mDrawPaint.setAntiAlias(true); - mDrawPaint.setStyle(Style.FILL); - - mMonthNumDrawPaint.setFakeBoldText(true); - mMonthNumDrawPaint.setAntiAlias(true); - mMonthNumDrawPaint.setStyle(Style.FILL); - mMonthNumDrawPaint.setTextAlign(Align.CENTER); - mMonthNumDrawPaint.setTextSize(mDateTextSize); + private void invalidateAllWeekViews() { + final int childCount = mListView.getChildCount(); + for (int i = 0; i < childCount; i++) { + View view = mListView.getChildAt(i); + view.invalidate(); + } } /** - * Returns the month of the first day in this week. + * Gets a calendar for locale bootstrapped with the value of a given calendar. * - * @return The month the first day of this view is in. + * @param oldCalendar The old calendar. + * @param locale The locale. */ - public int getMonthOfFirstWeekDay() { - return mMonthOfFirstWeekDay; + private static Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) { + if (oldCalendar == null) { + return Calendar.getInstance(locale); + } else { + final long currentTimeMillis = oldCalendar.getTimeInMillis(); + Calendar newCalendar = Calendar.getInstance(locale); + newCalendar.setTimeInMillis(currentTimeMillis); + return newCalendar; + } } /** - * Returns the month of the last day in this week - * - * @return The month the last day of this view is in + * @return True if the <code>firstDate</code> is the same as the <code> + * secondDate</code>. */ - public int getMonthOfLastWeekDay() { - return mLastWeekDayMonth; + private static boolean isSameDate(Calendar firstDate, Calendar secondDate) { + return (firstDate.get(Calendar.DAY_OF_YEAR) == secondDate.get(Calendar.DAY_OF_YEAR) + && firstDate.get(Calendar.YEAR) == secondDate.get(Calendar.YEAR)); } /** - * Returns the first day in this view. - * - * @return The first day in the view. + * Creates a new adapter if necessary and sets up its parameters. */ - public Calendar getFirstDay() { - return mFirstDay; + private void setUpAdapter() { + if (mAdapter == null) { + mAdapter = new WeeksAdapter(mContext); + mAdapter.registerDataSetObserver(new DataSetObserver() { + @Override + public void onChanged() { + if (mOnDateChangeListener != null) { + Calendar selectedDay = mAdapter.getSelectedDay(); + mOnDateChangeListener.onSelectedDayChange(mDelegator, + selectedDay.get(Calendar.YEAR), + selectedDay.get(Calendar.MONTH), + selectedDay.get(Calendar.DAY_OF_MONTH)); + } + } + }); + mListView.setAdapter(mAdapter); + } + + // refresh the view with the new parameters + mAdapter.notifyDataSetChanged(); } /** - * Calculates the day that the given x position is in, accounting for - * week number. - * - * @param x The x position of the touch event. - * @return True if a day was found for the given location. + * Sets up the strings to be used by the header. */ - public boolean getDayFromLocation(float x, Calendar outCalendar) { - final boolean isLayoutRtl = isLayoutRtl(); - - int start; - int end; + private void setUpHeader() { + mDayLabels = new String[mDaysPerWeek]; + for (int i = mFirstDayOfWeek, count = mFirstDayOfWeek + mDaysPerWeek; i < count; i++) { + int calendarDay = (i > Calendar.SATURDAY) ? i - Calendar.SATURDAY : i; + mDayLabels[i - mFirstDayOfWeek] = DateUtils.getDayOfWeekString(calendarDay, + DateUtils.LENGTH_SHORTEST); + } - if (isLayoutRtl) { - start = 0; - end = mShowWeekNumber ? mWidth - mWidth / mNumCells : mWidth; + TextView label = (TextView) mDayNamesHeader.getChildAt(0); + if (mShowWeekNumber) { + label.setVisibility(View.VISIBLE); } else { - start = mShowWeekNumber ? mWidth / mNumCells : 0; - end = mWidth; + label.setVisibility(View.GONE); } - - if (x < start || x > end) { - outCalendar.clear(); - return false; + for (int i = 1, count = mDayNamesHeader.getChildCount(); i < count; i++) { + label = (TextView) mDayNamesHeader.getChildAt(i); + if (mWeekDayTextAppearanceResId > -1) { + label.setTextAppearance(mContext, mWeekDayTextAppearanceResId); + } + if (i < mDaysPerWeek + 1) { + label.setText(mDayLabels[i - 1]); + label.setVisibility(View.VISIBLE); + } else { + label.setVisibility(View.GONE); + } } + mDayNamesHeader.invalidate(); + } + + /** + * Sets all the required fields for the list view. + */ + private void setUpListView() { + // Configure the listview + mListView.setDivider(null); + mListView.setItemsCanFocus(true); + mListView.setVerticalScrollBarEnabled(false); + mListView.setOnScrollListener(new OnScrollListener() { + public void onScrollStateChanged(AbsListView view, int scrollState) { + LegacyCalendarViewDelegate.this.onScrollStateChanged(view, scrollState); + } - // Selection is (x - start) / (pixels/day) which is (x - start) * day / pixels - int dayPosition = (int) ((x - start) * mDaysPerWeek / (end - start)); + public void onScroll( + AbsListView view, int firstVisibleItem, int visibleItemCount, + int totalItemCount) { + LegacyCalendarViewDelegate.this.onScroll(view, firstVisibleItem, + visibleItemCount, totalItemCount); + } + }); + // Make the scrolling behavior nicer + mListView.setFriction(mFriction); + mListView.setVelocityScale(mVelocityScale); + } - if (isLayoutRtl) { - dayPosition = mDaysPerWeek - 1 - dayPosition; + /** + * This moves to the specified time in the view. If the time is not already + * in range it will move the list so that the first of the month containing + * the time is at the top of the view. If the new time is already in view + * the list will not be scrolled unless forceScroll is true. This time may + * optionally be highlighted as selected as well. + * + * @param date The time to move to. + * @param animate Whether to scroll to the given time or just redraw at the + * new location. + * @param setSelected Whether to set the given time as selected. + * @param forceScroll Whether to recenter even if the time is already + * visible. + * + * @throws IllegalArgumentException of the provided date is before the + * range start of after the range end. + */ + private void goTo(Calendar date, boolean animate, boolean setSelected, + boolean forceScroll) { + if (date.before(mMinDate) || date.after(mMaxDate)) { + throw new IllegalArgumentException("Time not between " + mMinDate.getTime() + + " and " + mMaxDate.getTime()); + } + // Find the first and last entirely visible weeks + int firstFullyVisiblePosition = mListView.getFirstVisiblePosition(); + View firstChild = mListView.getChildAt(0); + if (firstChild != null && firstChild.getTop() < 0) { + firstFullyVisiblePosition++; + } + int lastFullyVisiblePosition = firstFullyVisiblePosition + mShownWeekCount - 1; + if (firstChild != null && firstChild.getTop() > mBottomBuffer) { + lastFullyVisiblePosition--; + } + if (setSelected) { + mAdapter.setSelectedDay(date); } + // Get the week we're going to + int position = getWeeksSinceMinDate(date); - outCalendar.setTimeInMillis(mFirstDay.getTimeInMillis()); - outCalendar.add(Calendar.DAY_OF_MONTH, dayPosition); + // Check if the selected day is now outside of our visible range + // and if so scroll to the month that contains it + if (position < firstFullyVisiblePosition || position > lastFullyVisiblePosition + || forceScroll) { + mFirstDayOfMonth.setTimeInMillis(date.getTimeInMillis()); + mFirstDayOfMonth.set(Calendar.DAY_OF_MONTH, 1); - return true; - } + setMonthDisplayed(mFirstDayOfMonth); - @Override - protected void onDraw(Canvas canvas) { - drawBackground(canvas); - drawWeekNumbersAndDates(canvas); - drawWeekSeparators(canvas); - drawSelectedDateVerticalBars(canvas); + // the earliest time we can scroll to is the min date + if (mFirstDayOfMonth.before(mMinDate)) { + position = 0; + } else { + position = getWeeksSinceMinDate(mFirstDayOfMonth); + } + + mPreviousScrollState = OnScrollListener.SCROLL_STATE_FLING; + if (animate) { + mListView.smoothScrollToPositionFromTop(position, mListScrollTopOffset, + GOTO_SCROLL_DURATION); + } else { + mListView.setSelectionFromTop(position, mListScrollTopOffset); + // Perform any after scroll operations that are needed + onScrollStateChanged(mListView, OnScrollListener.SCROLL_STATE_IDLE); + } + } else if (setSelected) { + // Otherwise just set the selection + setMonthDisplayed(date); + } } /** - * This draws the selection highlight if a day is selected in this week. + * Parses the given <code>date</code> and in case of success sets + * the result to the <code>outDate</code>. * - * @param canvas The canvas to draw on + * @return True if the date was parsed. */ - private void drawBackground(Canvas canvas) { - if (!mHasSelectedDay) { - return; + private boolean parseDate(String date, Calendar outDate) { + try { + outDate.setTime(mDateFormat.parse(date)); + return true; + } catch (ParseException e) { + Log.w(LOG_TAG, "Date: " + date + " not in format: " + DATE_FORMAT); + return false; } - mDrawPaint.setColor(mSelectedWeekBackgroundColor); + } - mTempRect.top = mWeekSeperatorLineWidth; - mTempRect.bottom = mHeight; + /** + * Called when a <code>view</code> transitions to a new <code>scrollState + * </code>. + */ + private void onScrollStateChanged(AbsListView view, int scrollState) { + mScrollStateChangedRunnable.doScrollStateChange(view, scrollState); + } - final boolean isLayoutRtl = isLayoutRtl(); + /** + * Updates the title and selected month if the <code>view</code> has moved to a new + * month. + */ + private void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, + int totalItemCount) { + WeekView child = (WeekView) view.getChildAt(0); + if (child == null) { + return; + } - if (isLayoutRtl) { - mTempRect.left = 0; - mTempRect.right = mSelectedLeft - 2; + // Figure out where we are + long currScroll = + view.getFirstVisiblePosition() * child.getHeight() - child.getBottom(); + + // If we have moved since our last call update the direction + if (currScroll < mPreviousScrollPosition) { + mIsScrollingUp = true; + } else if (currScroll > mPreviousScrollPosition) { + mIsScrollingUp = false; } else { - mTempRect.left = mShowWeekNumber ? mWidth / mNumCells : 0; - mTempRect.right = mSelectedLeft - 2; + return; } - canvas.drawRect(mTempRect, mDrawPaint); - if (isLayoutRtl) { - mTempRect.left = mSelectedRight + 3; - mTempRect.right = mShowWeekNumber ? mWidth - mWidth / mNumCells : mWidth; - } else { - mTempRect.left = mSelectedRight + 3; - mTempRect.right = mWidth; + // Use some hysteresis for checking which month to highlight. This + // causes the month to transition when two full weeks of a month are + // visible when scrolling up, and when the first day in a month reaches + // the top of the screen when scrolling down. + int offset = child.getBottom() < mWeekMinVisibleHeight ? 1 : 0; + if (mIsScrollingUp) { + child = (WeekView) view.getChildAt(SCROLL_HYST_WEEKS + offset); + } else if (offset != 0) { + child = (WeekView) view.getChildAt(offset); } - canvas.drawRect(mTempRect, mDrawPaint); - } - /** - * Draws the week and month day numbers for this week. - * - * @param canvas The canvas to draw on - */ - private void drawWeekNumbersAndDates(Canvas canvas) { - final float textHeight = mDrawPaint.getTextSize(); - final int y = (int) ((mHeight + textHeight) / 2) - mWeekSeperatorLineWidth; - final int nDays = mNumCells; - final int divisor = 2 * nDays; - - mDrawPaint.setTextAlign(Align.CENTER); - mDrawPaint.setTextSize(mDateTextSize); - - int i = 0; - - if (isLayoutRtl()) { - for (; i < nDays - 1; i++) { - mMonthNumDrawPaint.setColor(mFocusDay[i] ? mFocusedMonthDateColor - : mUnfocusedMonthDateColor); - int x = (2 * i + 1) * mWidth / divisor; - canvas.drawText(mDayNumbers[nDays - 1 - i], x, y, mMonthNumDrawPaint); - } - if (mShowWeekNumber) { - mDrawPaint.setColor(mWeekNumberColor); - int x = mWidth - mWidth / divisor; - canvas.drawText(mDayNumbers[0], x, y, mDrawPaint); + if (child != null) { + // Find out which month we're moving into + int month; + if (mIsScrollingUp) { + month = child.getMonthOfFirstWeekDay(); + } else { + month = child.getMonthOfLastWeekDay(); } - } else { - if (mShowWeekNumber) { - mDrawPaint.setColor(mWeekNumberColor); - int x = mWidth / divisor; - canvas.drawText(mDayNumbers[0], x, y, mDrawPaint); - i++; + + // And how it relates to our current highlighted month + int monthDiff; + if (mCurrentMonthDisplayed == 11 && month == 0) { + monthDiff = 1; + } else if (mCurrentMonthDisplayed == 0 && month == 11) { + monthDiff = -1; + } else { + monthDiff = month - mCurrentMonthDisplayed; } - for (; i < nDays; i++) { - mMonthNumDrawPaint.setColor(mFocusDay[i] ? mFocusedMonthDateColor - : mUnfocusedMonthDateColor); - int x = (2 * i + 1) * mWidth / divisor; - canvas.drawText(mDayNumbers[i], x, y, mMonthNumDrawPaint); + + // Only switch months if we're scrolling away from the currently + // selected month + if ((!mIsScrollingUp && monthDiff > 0) || (mIsScrollingUp && monthDiff < 0)) { + Calendar firstDay = child.getFirstDay(); + if (mIsScrollingUp) { + firstDay.add(Calendar.DAY_OF_MONTH, -DAYS_PER_WEEK); + } else { + firstDay.add(Calendar.DAY_OF_MONTH, DAYS_PER_WEEK); + } + setMonthDisplayed(firstDay); } } + mPreviousScrollPosition = currScroll; + mPreviousScrollState = mCurrentScrollState; } /** - * Draws a horizontal line for separating the weeks. + * Sets the month displayed at the top of this view based on time. Override + * to add custom events when the title is changed. * - * @param canvas The canvas to draw on. + * @param calendar A day in the new focus month. */ - private void drawWeekSeparators(Canvas canvas) { - // If it is the topmost fully visible child do not draw separator line - int firstFullyVisiblePosition = mListView.getFirstVisiblePosition(); - if (mListView.getChildAt(0).getTop() < 0) { - firstFullyVisiblePosition++; + private void setMonthDisplayed(Calendar calendar) { + mCurrentMonthDisplayed = calendar.get(Calendar.MONTH); + mAdapter.setFocusMonth(mCurrentMonthDisplayed); + final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_MONTH_DAY + | DateUtils.FORMAT_SHOW_YEAR; + final long millis = calendar.getTimeInMillis(); + String newMonthName = DateUtils.formatDateRange(mContext, millis, millis, flags); + mMonthName.setText(newMonthName); + mMonthName.invalidate(); + } + + /** + * @return Returns the number of weeks between the current <code>date</code> + * and the <code>mMinDate</code>. + */ + private int getWeeksSinceMinDate(Calendar date) { + if (date.before(mMinDate)) { + throw new IllegalArgumentException("fromDate: " + mMinDate.getTime() + + " does not precede toDate: " + date.getTime()); } - if (firstFullyVisiblePosition == mWeek) { - return; + long endTimeMillis = date.getTimeInMillis() + + date.getTimeZone().getOffset(date.getTimeInMillis()); + long startTimeMillis = mMinDate.getTimeInMillis() + + mMinDate.getTimeZone().getOffset(mMinDate.getTimeInMillis()); + long dayOffsetMillis = (mMinDate.get(Calendar.DAY_OF_WEEK) - mFirstDayOfWeek) + * MILLIS_IN_DAY; + return (int) ((endTimeMillis - startTimeMillis + dayOffsetMillis) / MILLIS_IN_WEEK); + } + + /** + * Command responsible for acting upon scroll state changes. + */ + private class ScrollStateRunnable implements Runnable { + private AbsListView mView; + + private int mNewState; + + /** + * Sets up the runnable with a short delay in case the scroll state + * immediately changes again. + * + * @param view The list view that changed state + * @param scrollState The new state it changed to + */ + public void doScrollStateChange(AbsListView view, int scrollState) { + mView = view; + mNewState = scrollState; + mDelegator.removeCallbacks(this); + mDelegator.postDelayed(this, SCROLL_CHANGE_DELAY); } - mDrawPaint.setColor(mWeekSeparatorLineColor); - mDrawPaint.setStrokeWidth(mWeekSeperatorLineWidth); - float startX; - float stopX; - if (isLayoutRtl()) { - startX = 0; - stopX = mShowWeekNumber ? mWidth - mWidth / mNumCells : mWidth; - } else { - startX = mShowWeekNumber ? mWidth / mNumCells : 0; - stopX = mWidth; + + public void run() { + mCurrentScrollState = mNewState; + // Fix the position after a scroll or a fling ends + if (mNewState == OnScrollListener.SCROLL_STATE_IDLE + && mPreviousScrollState != OnScrollListener.SCROLL_STATE_IDLE) { + View child = mView.getChildAt(0); + if (child == null) { + // The view is no longer visible, just return + return; + } + int dist = child.getBottom() - mListScrollTopOffset; + if (dist > mListScrollTopOffset) { + if (mIsScrollingUp) { + mView.smoothScrollBy(dist - child.getHeight(), + ADJUSTMENT_SCROLL_DURATION); + } else { + mView.smoothScrollBy(dist, ADJUSTMENT_SCROLL_DURATION); + } + } + } + mPreviousScrollState = mNewState; } - canvas.drawLine(startX, 0, stopX, 0, mDrawPaint); } /** - * Draws the selected date bars if this week has a selected day. - * - * @param canvas The canvas to draw on + * <p> + * This is a specialized adapter for creating a list of weeks with + * selectable days. It can be configured to display the week number, start + * the week on a given day, show a reduced number of days, or display an + * arbitrary number of weeks at a time. + * </p> */ - private void drawSelectedDateVerticalBars(Canvas canvas) { - if (!mHasSelectedDay) { - return; + private class WeeksAdapter extends BaseAdapter implements OnTouchListener { + + private int mSelectedWeek; + + private GestureDetector mGestureDetector; + + private int mFocusedMonth; + + private final Calendar mSelectedDate = Calendar.getInstance(); + + private int mTotalWeekCount; + + public WeeksAdapter(Context context) { + mContext = context; + mGestureDetector = new GestureDetector(mContext, new CalendarGestureListener()); + init(); } - mSelectedDateVerticalBar.setBounds(mSelectedLeft - mSelectedDateVerticalBarWidth / 2, - mWeekSeperatorLineWidth, - mSelectedLeft + mSelectedDateVerticalBarWidth / 2, mHeight); - mSelectedDateVerticalBar.draw(canvas); - mSelectedDateVerticalBar.setBounds(mSelectedRight - mSelectedDateVerticalBarWidth / 2, - mWeekSeperatorLineWidth, - mSelectedRight + mSelectedDateVerticalBarWidth / 2, mHeight); - mSelectedDateVerticalBar.draw(canvas); - } - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - mWidth = w; - updateSelectionPositions(); + /** + * Set up the gesture detector and selected time + */ + private void init() { + mSelectedWeek = getWeeksSinceMinDate(mSelectedDate); + mTotalWeekCount = getWeeksSinceMinDate(mMaxDate); + if (mMinDate.get(Calendar.DAY_OF_WEEK) != mFirstDayOfWeek + || mMaxDate.get(Calendar.DAY_OF_WEEK) != mFirstDayOfWeek) { + mTotalWeekCount++; + } + notifyDataSetChanged(); + } + + /** + * Updates the selected day and related parameters. + * + * @param selectedDay The time to highlight + */ + public void setSelectedDay(Calendar selectedDay) { + if (selectedDay.get(Calendar.DAY_OF_YEAR) == mSelectedDate.get(Calendar.DAY_OF_YEAR) + && selectedDay.get(Calendar.YEAR) == mSelectedDate.get(Calendar.YEAR)) { + return; + } + mSelectedDate.setTimeInMillis(selectedDay.getTimeInMillis()); + mSelectedWeek = getWeeksSinceMinDate(mSelectedDate); + mFocusedMonth = mSelectedDate.get(Calendar.MONTH); + notifyDataSetChanged(); + } + + /** + * @return The selected day of month. + */ + public Calendar getSelectedDay() { + return mSelectedDate; + } + + @Override + public int getCount() { + return mTotalWeekCount; + } + + @Override + public Object getItem(int position) { + return null; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + WeekView weekView = null; + if (convertView != null) { + weekView = (WeekView) convertView; + } else { + weekView = new WeekView(mContext); + android.widget.AbsListView.LayoutParams params = + new android.widget.AbsListView.LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT); + weekView.setLayoutParams(params); + weekView.setClickable(true); + weekView.setOnTouchListener(this); + } + + int selectedWeekDay = (mSelectedWeek == position) ? mSelectedDate.get( + Calendar.DAY_OF_WEEK) : -1; + weekView.init(position, selectedWeekDay, mFocusedMonth); + + return weekView; + } + + /** + * Changes which month is in focus and updates the view. + * + * @param month The month to show as in focus [0-11] + */ + public void setFocusMonth(int month) { + if (mFocusedMonth == month) { + return; + } + mFocusedMonth = month; + notifyDataSetChanged(); + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (mListView.isEnabled() && mGestureDetector.onTouchEvent(event)) { + WeekView weekView = (WeekView) v; + // if we cannot find a day for the given location we are done + if (!weekView.getDayFromLocation(event.getX(), mTempDate)) { + return true; + } + // it is possible that the touched day is outside the valid range + // we draw whole weeks but range end can fall not on the week end + if (mTempDate.before(mMinDate) || mTempDate.after(mMaxDate)) { + return true; + } + onDateTapped(mTempDate); + return true; + } + return false; + } + + /** + * Maintains the same hour/min/sec but moves the day to the tapped day. + * + * @param day The day that was tapped + */ + private void onDateTapped(Calendar day) { + setSelectedDay(day); + setMonthDisplayed(day); + } + + /** + * This is here so we can identify single tap events and set the + * selected day correctly + */ + class CalendarGestureListener extends GestureDetector.SimpleOnGestureListener { + @Override + public boolean onSingleTapUp(MotionEvent e) { + return true; + } + } } /** - * This calculates the positions for the selected day lines. + * <p> + * This is a dynamic view for drawing a single week. It can be configured to + * display the week number, start the week on a given day, or show a reduced + * number of days. It is intended for use as a single view within a + * ListView. See {@link WeeksAdapter} for usage. + * </p> */ - private void updateSelectionPositions() { - if (mHasSelectedDay) { + private class WeekView extends View { + + private final Rect mTempRect = new Rect(); + + private final Paint mDrawPaint = new Paint(); + + private final Paint mMonthNumDrawPaint = new Paint(); + + // Cache the number strings so we don't have to recompute them each time + private String[] mDayNumbers; + + // Quick lookup for checking which days are in the focus month + private boolean[] mFocusDay; + + // Whether this view has a focused day. + private boolean mHasFocusedDay; + + // Whether this view has only focused days. + private boolean mHasUnfocusedDay; + + // The first day displayed by this item + private Calendar mFirstDay; + + // The month of the first day in this week + private int mMonthOfFirstWeekDay = -1; + + // The month of the last day in this week + private int mLastWeekDayMonth = -1; + + // The position of this week, equivalent to weeks since the week of Jan + // 1st, 1900 + private int mWeek = -1; + + // Quick reference to the width of this view, matches parent + private int mWidth; + + // The height this view should draw at in pixels, set by height param + private int mHeight; + + // If this view contains the selected day + private boolean mHasSelectedDay = false; + + // Which day is selected [0-6] or -1 if no day is selected + private int mSelectedDay = -1; + + // The number of days + a spot for week number if it is displayed + private int mNumCells; + + // The left edge of the selected day + private int mSelectedLeft = -1; + + // The right edge of the selected day + private int mSelectedRight = -1; + + public WeekView(Context context) { + super(context); + + // Sets up any standard paints that will be used + initilaizePaints(); + } + + /** + * Initializes this week view. + * + * @param weekNumber The number of the week this view represents. The + * week number is a zero based index of the weeks since + * {@link CalendarView#getMinDate()}. + * @param selectedWeekDay The selected day of the week from 0 to 6, -1 if no + * selected day. + * @param focusedMonth The month that is currently in focus i.e. + * highlighted. + */ + public void init(int weekNumber, int selectedWeekDay, int focusedMonth) { + mSelectedDay = selectedWeekDay; + mHasSelectedDay = mSelectedDay != -1; + mNumCells = mShowWeekNumber ? mDaysPerWeek + 1 : mDaysPerWeek; + mWeek = weekNumber; + mTempDate.setTimeInMillis(mMinDate.getTimeInMillis()); + + mTempDate.add(Calendar.WEEK_OF_YEAR, mWeek); + mTempDate.setFirstDayOfWeek(mFirstDayOfWeek); + + // Allocate space for caching the day numbers and focus values + mDayNumbers = new String[mNumCells]; + mFocusDay = new boolean[mNumCells]; + + // If we're showing the week number calculate it based on Monday + int i = 0; + if (mShowWeekNumber) { + mDayNumbers[0] = String.format(Locale.getDefault(), "%d", + mTempDate.get(Calendar.WEEK_OF_YEAR)); + i++; + } + + // Now adjust our starting day based on the start day of the week + int diff = mFirstDayOfWeek - mTempDate.get(Calendar.DAY_OF_WEEK); + mTempDate.add(Calendar.DAY_OF_MONTH, diff); + + mFirstDay = (Calendar) mTempDate.clone(); + mMonthOfFirstWeekDay = mTempDate.get(Calendar.MONTH); + + mHasUnfocusedDay = true; + for (; i < mNumCells; i++) { + final boolean isFocusedDay = (mTempDate.get(Calendar.MONTH) == focusedMonth); + mFocusDay[i] = isFocusedDay; + mHasFocusedDay |= isFocusedDay; + mHasUnfocusedDay &= !isFocusedDay; + // do not draw dates outside the valid range to avoid user confusion + if (mTempDate.before(mMinDate) || mTempDate.after(mMaxDate)) { + mDayNumbers[i] = ""; + } else { + mDayNumbers[i] = String.format(Locale.getDefault(), "%d", + mTempDate.get(Calendar.DAY_OF_MONTH)); + } + mTempDate.add(Calendar.DAY_OF_MONTH, 1); + } + // We do one extra add at the end of the loop, if that pushed us to + // new month undo it + if (mTempDate.get(Calendar.DAY_OF_MONTH) == 1) { + mTempDate.add(Calendar.DAY_OF_MONTH, -1); + } + mLastWeekDayMonth = mTempDate.get(Calendar.MONTH); + + updateSelectionPositions(); + } + + /** + * Initialize the paint instances. + */ + private void initilaizePaints() { + mDrawPaint.setFakeBoldText(false); + mDrawPaint.setAntiAlias(true); + mDrawPaint.setStyle(Style.FILL); + + mMonthNumDrawPaint.setFakeBoldText(true); + mMonthNumDrawPaint.setAntiAlias(true); + mMonthNumDrawPaint.setStyle(Style.FILL); + mMonthNumDrawPaint.setTextAlign(Align.CENTER); + mMonthNumDrawPaint.setTextSize(mDateTextSize); + } + + /** + * Returns the month of the first day in this week. + * + * @return The month the first day of this view is in. + */ + public int getMonthOfFirstWeekDay() { + return mMonthOfFirstWeekDay; + } + + /** + * Returns the month of the last day in this week + * + * @return The month the last day of this view is in + */ + public int getMonthOfLastWeekDay() { + return mLastWeekDayMonth; + } + + /** + * Returns the first day in this view. + * + * @return The first day in the view. + */ + public Calendar getFirstDay() { + return mFirstDay; + } + + /** + * Calculates the day that the given x position is in, accounting for + * week number. + * + * @param x The x position of the touch event. + * @return True if a day was found for the given location. + */ + public boolean getDayFromLocation(float x, Calendar outCalendar) { final boolean isLayoutRtl = isLayoutRtl(); - int selectedPosition = mSelectedDay - mFirstDayOfWeek; - if (selectedPosition < 0) { - selectedPosition += 7; + + int start; + int end; + + if (isLayoutRtl) { + start = 0; + end = mShowWeekNumber ? mWidth - mWidth / mNumCells : mWidth; + } else { + start = mShowWeekNumber ? mWidth / mNumCells : 0; + end = mWidth; + } + + if (x < start || x > end) { + outCalendar.clear(); + return false; + } + + // Selection is (x - start) / (pixels/day) which is (x - start) * day / pixels + int dayPosition = (int) ((x - start) * mDaysPerWeek / (end - start)); + + if (isLayoutRtl) { + dayPosition = mDaysPerWeek - 1 - dayPosition; + } + + outCalendar.setTimeInMillis(mFirstDay.getTimeInMillis()); + outCalendar.add(Calendar.DAY_OF_MONTH, dayPosition); + + return true; + } + + @Override + protected void onDraw(Canvas canvas) { + drawBackground(canvas); + drawWeekNumbersAndDates(canvas); + drawWeekSeparators(canvas); + drawSelectedDateVerticalBars(canvas); + } + + /** + * This draws the selection highlight if a day is selected in this week. + * + * @param canvas The canvas to draw on + */ + private void drawBackground(Canvas canvas) { + if (!mHasSelectedDay) { + return; } - if (mShowWeekNumber && !isLayoutRtl) { - selectedPosition++; + mDrawPaint.setColor(mSelectedWeekBackgroundColor); + + mTempRect.top = mWeekSeperatorLineWidth; + mTempRect.bottom = mHeight; + + final boolean isLayoutRtl = isLayoutRtl(); + + if (isLayoutRtl) { + mTempRect.left = 0; + mTempRect.right = mSelectedLeft - 2; + } else { + mTempRect.left = mShowWeekNumber ? mWidth / mNumCells : 0; + mTempRect.right = mSelectedLeft - 2; } + canvas.drawRect(mTempRect, mDrawPaint); + if (isLayoutRtl) { - mSelectedLeft = (mDaysPerWeek - 1 - selectedPosition) * mWidth / mNumCells; + mTempRect.left = mSelectedRight + 3; + mTempRect.right = mShowWeekNumber ? mWidth - mWidth / mNumCells : mWidth; + } else { + mTempRect.left = mSelectedRight + 3; + mTempRect.right = mWidth; + } + canvas.drawRect(mTempRect, mDrawPaint); + } + /** + * Draws the week and month day numbers for this week. + * + * @param canvas The canvas to draw on + */ + private void drawWeekNumbersAndDates(Canvas canvas) { + final float textHeight = mDrawPaint.getTextSize(); + final int y = (int) ((mHeight + textHeight) / 2) - mWeekSeperatorLineWidth; + final int nDays = mNumCells; + final int divisor = 2 * nDays; + + mDrawPaint.setTextAlign(Align.CENTER); + mDrawPaint.setTextSize(mDateTextSize); + + int i = 0; + + if (isLayoutRtl()) { + for (; i < nDays - 1; i++) { + mMonthNumDrawPaint.setColor(mFocusDay[i] ? mFocusedMonthDateColor + : mUnfocusedMonthDateColor); + int x = (2 * i + 1) * mWidth / divisor; + canvas.drawText(mDayNumbers[nDays - 1 - i], x, y, mMonthNumDrawPaint); + } + if (mShowWeekNumber) { + mDrawPaint.setColor(mWeekNumberColor); + int x = mWidth - mWidth / divisor; + canvas.drawText(mDayNumbers[0], x, y, mDrawPaint); + } } else { - mSelectedLeft = selectedPosition * mWidth / mNumCells; + if (mShowWeekNumber) { + mDrawPaint.setColor(mWeekNumberColor); + int x = mWidth / divisor; + canvas.drawText(mDayNumbers[0], x, y, mDrawPaint); + i++; + } + for (; i < nDays; i++) { + mMonthNumDrawPaint.setColor(mFocusDay[i] ? mFocusedMonthDateColor + : mUnfocusedMonthDateColor); + int x = (2 * i + 1) * mWidth / divisor; + canvas.drawText(mDayNumbers[i], x, y, mMonthNumDrawPaint); + } } - mSelectedRight = mSelectedLeft + mWidth / mNumCells; } - } - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - mHeight = (mListView.getHeight() - mListView.getPaddingTop() - mListView - .getPaddingBottom()) / mShownWeekCount; - setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mHeight); + /** + * Draws a horizontal line for separating the weeks. + * + * @param canvas The canvas to draw on. + */ + private void drawWeekSeparators(Canvas canvas) { + // If it is the topmost fully visible child do not draw separator line + int firstFullyVisiblePosition = mListView.getFirstVisiblePosition(); + if (mListView.getChildAt(0).getTop() < 0) { + firstFullyVisiblePosition++; + } + if (firstFullyVisiblePosition == mWeek) { + return; + } + mDrawPaint.setColor(mWeekSeparatorLineColor); + mDrawPaint.setStrokeWidth(mWeekSeperatorLineWidth); + float startX; + float stopX; + if (isLayoutRtl()) { + startX = 0; + stopX = mShowWeekNumber ? mWidth - mWidth / mNumCells : mWidth; + } else { + startX = mShowWeekNumber ? mWidth / mNumCells : 0; + stopX = mWidth; + } + canvas.drawLine(startX, 0, stopX, 0, mDrawPaint); + } + + /** + * Draws the selected date bars if this week has a selected day. + * + * @param canvas The canvas to draw on + */ + private void drawSelectedDateVerticalBars(Canvas canvas) { + if (!mHasSelectedDay) { + return; + } + mSelectedDateVerticalBar.setBounds( + mSelectedLeft - mSelectedDateVerticalBarWidth / 2, + mWeekSeperatorLineWidth, + mSelectedLeft + mSelectedDateVerticalBarWidth / 2, + mHeight); + mSelectedDateVerticalBar.draw(canvas); + mSelectedDateVerticalBar.setBounds( + mSelectedRight - mSelectedDateVerticalBarWidth / 2, + mWeekSeperatorLineWidth, + mSelectedRight + mSelectedDateVerticalBarWidth / 2, + mHeight); + mSelectedDateVerticalBar.draw(canvas); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + mWidth = w; + updateSelectionPositions(); + } + + /** + * This calculates the positions for the selected day lines. + */ + private void updateSelectionPositions() { + if (mHasSelectedDay) { + final boolean isLayoutRtl = isLayoutRtl(); + int selectedPosition = mSelectedDay - mFirstDayOfWeek; + if (selectedPosition < 0) { + selectedPosition += 7; + } + if (mShowWeekNumber && !isLayoutRtl) { + selectedPosition++; + } + if (isLayoutRtl) { + mSelectedLeft = (mDaysPerWeek - 1 - selectedPosition) * mWidth / mNumCells; + + } else { + mSelectedLeft = selectedPosition * mWidth / mNumCells; + } + mSelectedRight = mSelectedLeft + mWidth / mNumCells; + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + mHeight = (mListView.getHeight() - mListView.getPaddingTop() - mListView + .getPaddingBottom()) / mShownWeekCount; + setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mHeight); + } } + } + } diff --git a/core/java/android/widget/CheckBox.java b/core/java/android/widget/CheckBox.java index f1804f8..71438c9 100644 --- a/core/java/android/widget/CheckBox.java +++ b/core/java/android/widget/CheckBox.java @@ -64,8 +64,12 @@ public class CheckBox extends CompoundButton { this(context, attrs, com.android.internal.R.attr.checkboxStyle); } - public CheckBox(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public CheckBox(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public CheckBox(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); } @Override diff --git a/core/java/android/widget/CheckedTextView.java b/core/java/android/widget/CheckedTextView.java index 5c10a77..1533510 100644 --- a/core/java/android/widget/CheckedTextView.java +++ b/core/java/android/widget/CheckedTextView.java @@ -58,11 +58,15 @@ public class CheckedTextView extends TextView implements Checkable { this(context, attrs, R.attr.checkedTextViewStyle); } - public CheckedTextView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public CheckedTextView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public CheckedTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = context.obtainStyledAttributes(attrs, - R.styleable.CheckedTextView, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.CheckedTextView, defStyleAttr, defStyleRes); Drawable d = a.getDrawable(R.styleable.CheckedTextView_checkMark); if (d != null) { @@ -119,7 +123,7 @@ public class CheckedTextView extends TextView implements Checkable { Drawable d = null; if (mCheckMarkResource != 0) { - d = getResources().getDrawable(mCheckMarkResource); + d = getContext().getDrawable(mCheckMarkResource); } setCheckMarkDrawable(d); } diff --git a/core/java/android/widget/Chronometer.java b/core/java/android/widget/Chronometer.java index b7a126e..f94789d 100644 --- a/core/java/android/widget/Chronometer.java +++ b/core/java/android/widget/Chronometer.java @@ -18,14 +18,12 @@ package android.widget; import android.content.Context; import android.content.res.TypedArray; -import android.graphics.Canvas; import android.os.Handler; import android.os.Message; import android.os.SystemClock; import android.text.format.DateUtils; import android.util.AttributeSet; import android.util.Log; -import android.util.Slog; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.RemoteViews.RemoteView; @@ -96,12 +94,15 @@ public class Chronometer extends TextView { * Initialize with standard view layout information and style. * Sets the base to the current time. */ - public Chronometer(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public Chronometer(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public Chronometer(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = context.obtainStyledAttributes( - attrs, - com.android.internal.R.styleable.Chronometer, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.Chronometer, defStyleAttr, defStyleRes); setFormat(a.getString(com.android.internal.R.styleable.Chronometer_format)); a.recycle(); diff --git a/core/java/android/widget/CompoundButton.java b/core/java/android/widget/CompoundButton.java index abddc90..4298545 100644 --- a/core/java/android/widget/CompoundButton.java +++ b/core/java/android/widget/CompoundButton.java @@ -64,12 +64,15 @@ public abstract class CompoundButton extends Button implements Checkable { this(context, attrs, 0); } - public CompoundButton(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public CompoundButton(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public CompoundButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = - context.obtainStyledAttributes( - attrs, com.android.internal.R.styleable.CompoundButton, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.CompoundButton, defStyleAttr, defStyleRes); Drawable d = a.getDrawable(com.android.internal.R.styleable.CompoundButton_button); if (d != null) { @@ -183,7 +186,7 @@ public abstract class CompoundButton extends Button implements Checkable { Drawable d = null; if (mButtonResource != 0) { - d = getResources().getDrawable(mButtonResource); + d = getContext().getDrawable(mButtonResource); } setButtonDrawable(d); } diff --git a/core/java/android/widget/DatePicker.java b/core/java/android/widget/DatePicker.java index d03161e..265dbcd 100644 --- a/core/java/android/widget/DatePicker.java +++ b/core/java/android/widget/DatePicker.java @@ -24,7 +24,6 @@ import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; import android.text.InputType; -import android.text.format.DateFormat; import android.text.format.DateUtils; import android.util.AttributeSet; import android.util.Log; @@ -76,53 +75,7 @@ public class DatePicker extends FrameLayout { private static final String LOG_TAG = DatePicker.class.getSimpleName(); - private static final String DATE_FORMAT = "MM/dd/yyyy"; - - private static final int DEFAULT_START_YEAR = 1900; - - private static final int DEFAULT_END_YEAR = 2100; - - private static final boolean DEFAULT_CALENDAR_VIEW_SHOWN = true; - - private static final boolean DEFAULT_SPINNERS_SHOWN = true; - - private static final boolean DEFAULT_ENABLED_STATE = true; - - private final LinearLayout mSpinners; - - private final NumberPicker mDaySpinner; - - private final NumberPicker mMonthSpinner; - - private final NumberPicker mYearSpinner; - - private final EditText mDaySpinnerInput; - - private final EditText mMonthSpinnerInput; - - private final EditText mYearSpinnerInput; - - private final CalendarView mCalendarView; - - private Locale mCurrentLocale; - - private OnDateChangedListener mOnDateChangedListener; - - private String[] mShortMonths; - - private final java.text.DateFormat mDateFormat = new SimpleDateFormat(DATE_FORMAT); - - private int mNumberOfMonths; - - private Calendar mTempDate; - - private Calendar mMinDate; - - private Calendar mMaxDate; - - private Calendar mCurrentDate; - - private boolean mIsEnabled = DEFAULT_ENABLED_STATE; + private DatePickerDelegate mDelegate; /** * The callback used to indicate the user changes\d the date. @@ -149,147 +102,61 @@ public class DatePicker extends FrameLayout { this(context, attrs, R.attr.datePickerStyle); } - public DatePicker(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - // initialization based on locale - setCurrentLocale(Locale.getDefault()); - - TypedArray attributesArray = context.obtainStyledAttributes(attrs, R.styleable.DatePicker, - defStyle, 0); - boolean spinnersShown = attributesArray.getBoolean(R.styleable.DatePicker_spinnersShown, - DEFAULT_SPINNERS_SHOWN); - boolean calendarViewShown = attributesArray.getBoolean( - R.styleable.DatePicker_calendarViewShown, DEFAULT_CALENDAR_VIEW_SHOWN); - int startYear = attributesArray.getInt(R.styleable.DatePicker_startYear, - DEFAULT_START_YEAR); - int endYear = attributesArray.getInt(R.styleable.DatePicker_endYear, DEFAULT_END_YEAR); - String minDate = attributesArray.getString(R.styleable.DatePicker_minDate); - String maxDate = attributesArray.getString(R.styleable.DatePicker_maxDate); - int layoutResourceId = attributesArray.getResourceId(R.styleable.DatePicker_internalLayout, - R.layout.date_picker); - attributesArray.recycle(); - - LayoutInflater inflater = (LayoutInflater) context - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - inflater.inflate(layoutResourceId, this, true); - - OnValueChangeListener onChangeListener = new OnValueChangeListener() { - public void onValueChange(NumberPicker picker, int oldVal, int newVal) { - updateInputState(); - mTempDate.setTimeInMillis(mCurrentDate.getTimeInMillis()); - // take care of wrapping of days and months to update greater fields - if (picker == mDaySpinner) { - int maxDayOfMonth = mTempDate.getActualMaximum(Calendar.DAY_OF_MONTH); - if (oldVal == maxDayOfMonth && newVal == 1) { - mTempDate.add(Calendar.DAY_OF_MONTH, 1); - } else if (oldVal == 1 && newVal == maxDayOfMonth) { - mTempDate.add(Calendar.DAY_OF_MONTH, -1); - } else { - mTempDate.add(Calendar.DAY_OF_MONTH, newVal - oldVal); - } - } else if (picker == mMonthSpinner) { - if (oldVal == 11 && newVal == 0) { - mTempDate.add(Calendar.MONTH, 1); - } else if (oldVal == 0 && newVal == 11) { - mTempDate.add(Calendar.MONTH, -1); - } else { - mTempDate.add(Calendar.MONTH, newVal - oldVal); - } - } else if (picker == mYearSpinner) { - mTempDate.set(Calendar.YEAR, newVal); - } else { - throw new IllegalArgumentException(); - } - // now set the date to the adjusted one - setDate(mTempDate.get(Calendar.YEAR), mTempDate.get(Calendar.MONTH), - mTempDate.get(Calendar.DAY_OF_MONTH)); - updateSpinners(); - updateCalendarView(); - notifyDateChanged(); - } - }; + public DatePicker(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } - mSpinners = (LinearLayout) findViewById(R.id.pickers); + public DatePicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - // calendar view day-picker - mCalendarView = (CalendarView) findViewById(R.id.calendar_view); - mCalendarView.setOnDateChangeListener(new CalendarView.OnDateChangeListener() { - public void onSelectedDayChange(CalendarView view, int year, int month, int monthDay) { - setDate(year, month, monthDay); - updateSpinners(); - notifyDateChanged(); - } - }); - - // day - mDaySpinner = (NumberPicker) findViewById(R.id.day); - mDaySpinner.setFormatter(NumberPicker.getTwoDigitFormatter()); - mDaySpinner.setOnLongPressUpdateInterval(100); - mDaySpinner.setOnValueChangedListener(onChangeListener); - mDaySpinnerInput = (EditText) mDaySpinner.findViewById(R.id.numberpicker_input); - - // month - mMonthSpinner = (NumberPicker) findViewById(R.id.month); - mMonthSpinner.setMinValue(0); - mMonthSpinner.setMaxValue(mNumberOfMonths - 1); - mMonthSpinner.setDisplayedValues(mShortMonths); - mMonthSpinner.setOnLongPressUpdateInterval(200); - mMonthSpinner.setOnValueChangedListener(onChangeListener); - mMonthSpinnerInput = (EditText) mMonthSpinner.findViewById(R.id.numberpicker_input); - - // year - mYearSpinner = (NumberPicker) findViewById(R.id.year); - mYearSpinner.setOnLongPressUpdateInterval(100); - mYearSpinner.setOnValueChangedListener(onChangeListener); - mYearSpinnerInput = (EditText) mYearSpinner.findViewById(R.id.numberpicker_input); - - // show only what the user required but make sure we - // show something and the spinners have higher priority - if (!spinnersShown && !calendarViewShown) { - setSpinnersShown(true); - } else { - setSpinnersShown(spinnersShown); - setCalendarViewShown(calendarViewShown); - } - - // set the min date giving priority of the minDate over startYear - mTempDate.clear(); - if (!TextUtils.isEmpty(minDate)) { - if (!parseDate(minDate, mTempDate)) { - mTempDate.set(startYear, 0, 1); - } - } else { - mTempDate.set(startYear, 0, 1); - } - setMinDate(mTempDate.getTimeInMillis()); + mDelegate = new LegacyDatePickerDelegate(this, context, attrs, defStyleAttr, defStyleRes); + } - // set the max date giving priority of the maxDate over endYear - mTempDate.clear(); - if (!TextUtils.isEmpty(maxDate)) { - if (!parseDate(maxDate, mTempDate)) { - mTempDate.set(endYear, 11, 31); - } - } else { - mTempDate.set(endYear, 11, 31); - } - setMaxDate(mTempDate.getTimeInMillis()); + /** + * Initialize the state. If the provided values designate an inconsistent + * date the values are normalized before updating the spinners. + * + * @param year The initial year. + * @param monthOfYear The initial month <strong>starting from zero</strong>. + * @param dayOfMonth The initial day of the month. + * @param onDateChangedListener How user is notified date is changed by + * user, can be null. + */ + public void init(int year, int monthOfYear, int dayOfMonth, + OnDateChangedListener onDateChangedListener) { + mDelegate.init(year, monthOfYear, dayOfMonth, onDateChangedListener); + } - // initialize to current date - mCurrentDate.setTimeInMillis(System.currentTimeMillis()); - init(mCurrentDate.get(Calendar.YEAR), mCurrentDate.get(Calendar.MONTH), mCurrentDate - .get(Calendar.DAY_OF_MONTH), null); + /** + * Updates the current date. + * + * @param year The year. + * @param month The month which is <strong>starting from zero</strong>. + * @param dayOfMonth The day of the month. + */ + public void updateDate(int year, int month, int dayOfMonth) { + mDelegate.updateDate(year, month, dayOfMonth); + } - // re-order the number spinners to match the current date format - reorderSpinners(); + /** + * @return The selected year. + */ + public int getYear() { + return mDelegate.getYear(); + } - // accessibility - setContentDescriptions(); + /** + * @return The selected month. + */ + public int getMonth() { + return mDelegate.getMonth(); + } - // If not explicitly specified this view is important for accessibility. - if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { - setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); - } + /** + * @return The selected day of month. + */ + public int getDayOfMonth() { + return mDelegate.getDayOfMonth(); } /** @@ -303,7 +170,7 @@ public class DatePicker extends FrameLayout { * @return The minimal supported date. */ public long getMinDate() { - return mCalendarView.getMinDate(); + return mDelegate.getMinDate(); } /** @@ -314,18 +181,7 @@ public class DatePicker extends FrameLayout { * @param minDate The minimal supported date. */ public void setMinDate(long minDate) { - mTempDate.setTimeInMillis(minDate); - if (mTempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR) - && mTempDate.get(Calendar.DAY_OF_YEAR) != mMinDate.get(Calendar.DAY_OF_YEAR)) { - return; - } - mMinDate.setTimeInMillis(minDate); - mCalendarView.setMinDate(minDate); - if (mCurrentDate.before(mMinDate)) { - mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis()); - updateCalendarView(); - } - updateSpinners(); + mDelegate.setMinDate(minDate); } /** @@ -339,7 +195,7 @@ public class DatePicker extends FrameLayout { * @return The maximal supported date. */ public long getMaxDate() { - return mCalendarView.getMaxDate(); + return mDelegate.getMaxDate(); } /** @@ -350,70 +206,50 @@ public class DatePicker extends FrameLayout { * @param maxDate The maximal supported date. */ public void setMaxDate(long maxDate) { - mTempDate.setTimeInMillis(maxDate); - if (mTempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR) - && mTempDate.get(Calendar.DAY_OF_YEAR) != mMaxDate.get(Calendar.DAY_OF_YEAR)) { - return; - } - mMaxDate.setTimeInMillis(maxDate); - mCalendarView.setMaxDate(maxDate); - if (mCurrentDate.after(mMaxDate)) { - mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis()); - updateCalendarView(); - } - updateSpinners(); + mDelegate.setMaxDate(maxDate); } @Override public void setEnabled(boolean enabled) { - if (mIsEnabled == enabled) { + if (mDelegate.isEnabled() == enabled) { return; } super.setEnabled(enabled); - mDaySpinner.setEnabled(enabled); - mMonthSpinner.setEnabled(enabled); - mYearSpinner.setEnabled(enabled); - mCalendarView.setEnabled(enabled); - mIsEnabled = enabled; + mDelegate.setEnabled(enabled); } @Override public boolean isEnabled() { - return mIsEnabled; + return mDelegate.isEnabled(); } @Override public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - onPopulateAccessibilityEvent(event); - return true; + return mDelegate.dispatchPopulateAccessibilityEvent(event); } @Override public void onPopulateAccessibilityEvent(AccessibilityEvent event) { super.onPopulateAccessibilityEvent(event); - - final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR; - String selectedDateUtterance = DateUtils.formatDateTime(mContext, - mCurrentDate.getTimeInMillis(), flags); - event.getText().add(selectedDateUtterance); + mDelegate.onPopulateAccessibilityEvent(event); } @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); - event.setClassName(DatePicker.class.getName()); + mDelegate.onInitializeAccessibilityEvent(event); } @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(DatePicker.class.getName()); + mDelegate.onInitializeAccessibilityNodeInfo(info); } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); - setCurrentLocale(newConfig.locale); + mDelegate.onConfigurationChanged(newConfig); } /** @@ -423,7 +259,7 @@ public class DatePicker extends FrameLayout { * @see #getCalendarView() */ public boolean getCalendarViewShown() { - return (mCalendarView.getVisibility() == View.VISIBLE); + return mDelegate.getCalendarViewShown(); } /** @@ -433,7 +269,7 @@ public class DatePicker extends FrameLayout { * @see #getCalendarViewShown() */ public CalendarView getCalendarView () { - return mCalendarView; + return mDelegate.getCalendarView(); } /** @@ -442,7 +278,7 @@ public class DatePicker extends FrameLayout { * @param shown True if the calendar view is to be shown. */ public void setCalendarViewShown(boolean shown) { - mCalendarView.setVisibility(shown ? VISIBLE : GONE); + mDelegate.setCalendarViewShown(shown); } /** @@ -451,7 +287,7 @@ public class DatePicker extends FrameLayout { * @return True if the spinners are shown. */ public boolean getSpinnersShown() { - return mSpinners.isShown(); + return mDelegate.getSpinnersShown(); } /** @@ -460,330 +296,708 @@ public class DatePicker extends FrameLayout { * @param shown True if the spinners are to be shown. */ public void setSpinnersShown(boolean shown) { - mSpinners.setVisibility(shown ? VISIBLE : GONE); + mDelegate.setSpinnersShown(shown); + } + + // Override so we are in complete control of save / restore for this widget. + @Override + protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) { + mDelegate.dispatchRestoreInstanceState(container); + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + return mDelegate.onSaveInstanceState(superState); + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + mDelegate.onRestoreInstanceState(ss); } /** - * Sets the current locale. - * - * @param locale The current locale. + * A delegate interface that defined the public API of the DatePicker. Allows different + * DatePicker implementations. This would need to be implemented by the DatePicker delegates + * for the real behavior. */ - private void setCurrentLocale(Locale locale) { - if (locale.equals(mCurrentLocale)) { - return; - } + interface DatePickerDelegate { + void init(int year, int monthOfYear, int dayOfMonth, + OnDateChangedListener onDateChangedListener); - mCurrentLocale = locale; + void updateDate(int year, int month, int dayOfMonth); - mTempDate = getCalendarForLocale(mTempDate, locale); - mMinDate = getCalendarForLocale(mMinDate, locale); - mMaxDate = getCalendarForLocale(mMaxDate, locale); - mCurrentDate = getCalendarForLocale(mCurrentDate, locale); + int getYear(); + int getMonth(); + int getDayOfMonth(); - mNumberOfMonths = mTempDate.getActualMaximum(Calendar.MONTH) + 1; - mShortMonths = new DateFormatSymbols().getShortMonths(); + void setMinDate(long minDate); + long getMinDate(); - if (usingNumericMonths()) { - // We're in a locale where a date should either be all-numeric, or all-text. - // All-text would require custom NumberPicker formatters for day and year. - mShortMonths = new String[mNumberOfMonths]; - for (int i = 0; i < mNumberOfMonths; ++i) { - mShortMonths[i] = String.format("%d", i + 1); - } - } - } + void setMaxDate(long maxDate); + long getMaxDate(); - /** - * Tests whether the current locale is one where there are no real month names, - * such as Chinese, Japanese, or Korean locales. - */ - private boolean usingNumericMonths() { - return Character.isDigit(mShortMonths[Calendar.JANUARY].charAt(0)); + void setEnabled(boolean enabled); + boolean isEnabled(); + + CalendarView getCalendarView (); + + void setCalendarViewShown(boolean shown); + boolean getCalendarViewShown(); + + void setSpinnersShown(boolean shown); + boolean getSpinnersShown(); + + void onConfigurationChanged(Configuration newConfig); + + void dispatchRestoreInstanceState(SparseArray<Parcelable> container); + Parcelable onSaveInstanceState(Parcelable superState); + void onRestoreInstanceState(Parcelable state); + + boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event); + void onPopulateAccessibilityEvent(AccessibilityEvent event); + void onInitializeAccessibilityEvent(AccessibilityEvent event); + void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info); } /** - * Gets a calendar for locale bootstrapped with the value of a given calendar. - * - * @param oldCalendar The old calendar. - * @param locale The locale. + * An abstract class which can be used as a start for DatePicker implementations */ - private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) { - if (oldCalendar == null) { - return Calendar.getInstance(locale); - } else { - final long currentTimeMillis = oldCalendar.getTimeInMillis(); - Calendar newCalendar = Calendar.getInstance(locale); - newCalendar.setTimeInMillis(currentTimeMillis); - return newCalendar; + abstract static class AbstractTimePickerDelegate implements DatePickerDelegate { + // The delegator + protected DatePicker mDelegator; + + // The context + protected Context mContext; + + // The current locale + protected Locale mCurrentLocale; + + // Callbacks + protected OnDateChangedListener mOnDateChangedListener; + + public AbstractTimePickerDelegate(DatePicker delegator, Context context) { + mDelegator = delegator; + mContext = context; + + // initialization based on locale + setCurrentLocale(Locale.getDefault()); } - } - /** - * Reorders the spinners according to the date format that is - * explicitly set by the user and if no such is set fall back - * to the current locale's default format. - */ - private void reorderSpinners() { - mSpinners.removeAllViews(); - // We use numeric spinners for year and day, but textual months. Ask icu4c what - // order the user's locale uses for that combination. http://b/7207103. - String pattern = ICU.getBestDateTimePattern("yyyyMMMdd", Locale.getDefault().toString()); - char[] order = ICU.getDateFormatOrder(pattern); - final int spinnerCount = order.length; - for (int i = 0; i < spinnerCount; i++) { - switch (order[i]) { - case 'd': - mSpinners.addView(mDaySpinner); - setImeOptions(mDaySpinner, spinnerCount, i); - break; - case 'M': - mSpinners.addView(mMonthSpinner); - setImeOptions(mMonthSpinner, spinnerCount, i); - break; - case 'y': - mSpinners.addView(mYearSpinner); - setImeOptions(mYearSpinner, spinnerCount, i); - break; - default: - throw new IllegalArgumentException(Arrays.toString(order)); + protected void setCurrentLocale(Locale locale) { + if (locale.equals(mCurrentLocale)) { + return; } + mCurrentLocale = locale; } } /** - * Updates the current date. - * - * @param year The year. - * @param month The month which is <strong>starting from zero</strong>. - * @param dayOfMonth The day of the month. + * A delegate implementing the basic DatePicker */ - public void updateDate(int year, int month, int dayOfMonth) { - if (!isNewDate(year, month, dayOfMonth)) { - return; + private static class LegacyDatePickerDelegate extends AbstractTimePickerDelegate { + + private static final String DATE_FORMAT = "MM/dd/yyyy"; + + private static final int DEFAULT_START_YEAR = 1900; + + private static final int DEFAULT_END_YEAR = 2100; + + private static final boolean DEFAULT_CALENDAR_VIEW_SHOWN = true; + + private static final boolean DEFAULT_SPINNERS_SHOWN = true; + + private static final boolean DEFAULT_ENABLED_STATE = true; + + private final LinearLayout mSpinners; + + private final NumberPicker mDaySpinner; + + private final NumberPicker mMonthSpinner; + + private final NumberPicker mYearSpinner; + + private final EditText mDaySpinnerInput; + + private final EditText mMonthSpinnerInput; + + private final EditText mYearSpinnerInput; + + private final CalendarView mCalendarView; + + private String[] mShortMonths; + + private final java.text.DateFormat mDateFormat = new SimpleDateFormat(DATE_FORMAT); + + private int mNumberOfMonths; + + private Calendar mTempDate; + + private Calendar mMinDate; + + private Calendar mMaxDate; + + private Calendar mCurrentDate; + + private boolean mIsEnabled = DEFAULT_ENABLED_STATE; + + LegacyDatePickerDelegate(DatePicker delegator, Context context, AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + super(delegator, context); + + mDelegator = delegator; + mContext = context; + + // initialization based on locale + setCurrentLocale(Locale.getDefault()); + + final TypedArray attributesArray = context.obtainStyledAttributes(attrs, + R.styleable.DatePicker, defStyleAttr, defStyleRes); + boolean spinnersShown = attributesArray.getBoolean(R.styleable.DatePicker_spinnersShown, + DEFAULT_SPINNERS_SHOWN); + boolean calendarViewShown = attributesArray.getBoolean( + R.styleable.DatePicker_calendarViewShown, DEFAULT_CALENDAR_VIEW_SHOWN); + int startYear = attributesArray.getInt(R.styleable.DatePicker_startYear, + DEFAULT_START_YEAR); + int endYear = attributesArray.getInt(R.styleable.DatePicker_endYear, DEFAULT_END_YEAR); + String minDate = attributesArray.getString(R.styleable.DatePicker_minDate); + String maxDate = attributesArray.getString(R.styleable.DatePicker_maxDate); + int layoutResourceId = attributesArray.getResourceId( + R.styleable.DatePicker_internalLayout, R.layout.date_picker); + attributesArray.recycle(); + + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(layoutResourceId, mDelegator, true); + + OnValueChangeListener onChangeListener = new OnValueChangeListener() { + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + updateInputState(); + mTempDate.setTimeInMillis(mCurrentDate.getTimeInMillis()); + // take care of wrapping of days and months to update greater fields + if (picker == mDaySpinner) { + int maxDayOfMonth = mTempDate.getActualMaximum(Calendar.DAY_OF_MONTH); + if (oldVal == maxDayOfMonth && newVal == 1) { + mTempDate.add(Calendar.DAY_OF_MONTH, 1); + } else if (oldVal == 1 && newVal == maxDayOfMonth) { + mTempDate.add(Calendar.DAY_OF_MONTH, -1); + } else { + mTempDate.add(Calendar.DAY_OF_MONTH, newVal - oldVal); + } + } else if (picker == mMonthSpinner) { + if (oldVal == 11 && newVal == 0) { + mTempDate.add(Calendar.MONTH, 1); + } else if (oldVal == 0 && newVal == 11) { + mTempDate.add(Calendar.MONTH, -1); + } else { + mTempDate.add(Calendar.MONTH, newVal - oldVal); + } + } else if (picker == mYearSpinner) { + mTempDate.set(Calendar.YEAR, newVal); + } else { + throw new IllegalArgumentException(); + } + // now set the date to the adjusted one + setDate(mTempDate.get(Calendar.YEAR), mTempDate.get(Calendar.MONTH), + mTempDate.get(Calendar.DAY_OF_MONTH)); + updateSpinners(); + updateCalendarView(); + notifyDateChanged(); + } + }; + + mSpinners = (LinearLayout) mDelegator.findViewById(R.id.pickers); + + // calendar view day-picker + mCalendarView = (CalendarView) mDelegator.findViewById(R.id.calendar_view); + mCalendarView.setOnDateChangeListener(new CalendarView.OnDateChangeListener() { + public void onSelectedDayChange(CalendarView view, int year, int month, int monthDay) { + setDate(year, month, monthDay); + updateSpinners(); + notifyDateChanged(); + } + }); + + // day + mDaySpinner = (NumberPicker) mDelegator.findViewById(R.id.day); + mDaySpinner.setFormatter(NumberPicker.getTwoDigitFormatter()); + mDaySpinner.setOnLongPressUpdateInterval(100); + mDaySpinner.setOnValueChangedListener(onChangeListener); + mDaySpinnerInput = (EditText) mDaySpinner.findViewById(R.id.numberpicker_input); + + // month + mMonthSpinner = (NumberPicker) mDelegator.findViewById(R.id.month); + mMonthSpinner.setMinValue(0); + mMonthSpinner.setMaxValue(mNumberOfMonths - 1); + mMonthSpinner.setDisplayedValues(mShortMonths); + mMonthSpinner.setOnLongPressUpdateInterval(200); + mMonthSpinner.setOnValueChangedListener(onChangeListener); + mMonthSpinnerInput = (EditText) mMonthSpinner.findViewById(R.id.numberpicker_input); + + // year + mYearSpinner = (NumberPicker) mDelegator.findViewById(R.id.year); + mYearSpinner.setOnLongPressUpdateInterval(100); + mYearSpinner.setOnValueChangedListener(onChangeListener); + mYearSpinnerInput = (EditText) mYearSpinner.findViewById(R.id.numberpicker_input); + + // show only what the user required but make sure we + // show something and the spinners have higher priority + if (!spinnersShown && !calendarViewShown) { + setSpinnersShown(true); + } else { + setSpinnersShown(spinnersShown); + setCalendarViewShown(calendarViewShown); + } + + // set the min date giving priority of the minDate over startYear + mTempDate.clear(); + if (!TextUtils.isEmpty(minDate)) { + if (!parseDate(minDate, mTempDate)) { + mTempDate.set(startYear, 0, 1); + } + } else { + mTempDate.set(startYear, 0, 1); + } + setMinDate(mTempDate.getTimeInMillis()); + + // set the max date giving priority of the maxDate over endYear + mTempDate.clear(); + if (!TextUtils.isEmpty(maxDate)) { + if (!parseDate(maxDate, mTempDate)) { + mTempDate.set(endYear, 11, 31); + } + } else { + mTempDate.set(endYear, 11, 31); + } + setMaxDate(mTempDate.getTimeInMillis()); + + // initialize to current date + mCurrentDate.setTimeInMillis(System.currentTimeMillis()); + init(mCurrentDate.get(Calendar.YEAR), mCurrentDate.get(Calendar.MONTH), mCurrentDate + .get(Calendar.DAY_OF_MONTH), null); + + // re-order the number spinners to match the current date format + reorderSpinners(); + + // accessibility + setContentDescriptions(); + + // If not explicitly specified this view is important for accessibility. + if (mDelegator.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + mDelegator.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + } } - setDate(year, month, dayOfMonth); - updateSpinners(); - updateCalendarView(); - notifyDateChanged(); - } - // Override so we are in complete control of save / restore for this widget. - @Override - protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) { - dispatchThawSelfOnly(container); - } + @Override + public void init(int year, int monthOfYear, int dayOfMonth, + OnDateChangedListener onDateChangedListener) { + setDate(year, monthOfYear, dayOfMonth); + updateSpinners(); + updateCalendarView(); + mOnDateChangedListener = onDateChangedListener; + } - @Override - protected Parcelable onSaveInstanceState() { - Parcelable superState = super.onSaveInstanceState(); - return new SavedState(superState, getYear(), getMonth(), getDayOfMonth()); - } + @Override + public void updateDate(int year, int month, int dayOfMonth) { + if (!isNewDate(year, month, dayOfMonth)) { + return; + } + setDate(year, month, dayOfMonth); + updateSpinners(); + updateCalendarView(); + notifyDateChanged(); + } - @Override - protected void onRestoreInstanceState(Parcelable state) { - SavedState ss = (SavedState) state; - super.onRestoreInstanceState(ss.getSuperState()); - setDate(ss.mYear, ss.mMonth, ss.mDay); - updateSpinners(); - updateCalendarView(); - } + @Override + public int getYear() { + return mCurrentDate.get(Calendar.YEAR); + } - /** - * Initialize the state. If the provided values designate an inconsistent - * date the values are normalized before updating the spinners. - * - * @param year The initial year. - * @param monthOfYear The initial month <strong>starting from zero</strong>. - * @param dayOfMonth The initial day of the month. - * @param onDateChangedListener How user is notified date is changed by - * user, can be null. - */ - public void init(int year, int monthOfYear, int dayOfMonth, - OnDateChangedListener onDateChangedListener) { - setDate(year, monthOfYear, dayOfMonth); - updateSpinners(); - updateCalendarView(); - mOnDateChangedListener = onDateChangedListener; - } + @Override + public int getMonth() { + return mCurrentDate.get(Calendar.MONTH); + } - /** - * Parses the given <code>date</code> and in case of success sets the result - * to the <code>outDate</code>. - * - * @return True if the date was parsed. - */ - private boolean parseDate(String date, Calendar outDate) { - try { - outDate.setTime(mDateFormat.parse(date)); + @Override + public int getDayOfMonth() { + return mCurrentDate.get(Calendar.DAY_OF_MONTH); + } + + @Override + public void setMinDate(long minDate) { + mTempDate.setTimeInMillis(minDate); + if (mTempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR) + && mTempDate.get(Calendar.DAY_OF_YEAR) != mMinDate.get(Calendar.DAY_OF_YEAR)) { + return; + } + mMinDate.setTimeInMillis(minDate); + mCalendarView.setMinDate(minDate); + if (mCurrentDate.before(mMinDate)) { + mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis()); + updateCalendarView(); + } + updateSpinners(); + } + + @Override + public long getMinDate() { + return mCalendarView.getMinDate(); + } + + @Override + public void setMaxDate(long maxDate) { + mTempDate.setTimeInMillis(maxDate); + if (mTempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR) + && mTempDate.get(Calendar.DAY_OF_YEAR) != mMaxDate.get(Calendar.DAY_OF_YEAR)) { + return; + } + mMaxDate.setTimeInMillis(maxDate); + mCalendarView.setMaxDate(maxDate); + if (mCurrentDate.after(mMaxDate)) { + mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis()); + updateCalendarView(); + } + updateSpinners(); + } + + @Override + public long getMaxDate() { + return mCalendarView.getMaxDate(); + } + + @Override + public void setEnabled(boolean enabled) { + mDaySpinner.setEnabled(enabled); + mMonthSpinner.setEnabled(enabled); + mYearSpinner.setEnabled(enabled); + mCalendarView.setEnabled(enabled); + mIsEnabled = enabled; + } + + @Override + public boolean isEnabled() { + return mIsEnabled; + } + + @Override + public CalendarView getCalendarView() { + return mCalendarView; + } + + @Override + public void setCalendarViewShown(boolean shown) { + mCalendarView.setVisibility(shown ? VISIBLE : GONE); + } + + @Override + public boolean getCalendarViewShown() { + return (mCalendarView.getVisibility() == View.VISIBLE); + } + + @Override + public void setSpinnersShown(boolean shown) { + mSpinners.setVisibility(shown ? VISIBLE : GONE); + } + + @Override + public boolean getSpinnersShown() { + return mSpinners.isShown(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + setCurrentLocale(newConfig.locale); + } + + @Override + public void dispatchRestoreInstanceState(SparseArray<Parcelable> container) { + mDelegator.dispatchThawSelfOnly(container); + } + + @Override + public Parcelable onSaveInstanceState(Parcelable superState) { + return new SavedState(superState, getYear(), getMonth(), getDayOfMonth()); + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + setDate(ss.mYear, ss.mMonth, ss.mDay); + updateSpinners(); + updateCalendarView(); + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + onPopulateAccessibilityEvent(event); return true; - } catch (ParseException e) { - Log.w(LOG_TAG, "Date: " + date + " not in format: " + DATE_FORMAT); - return false; - } - } - - private boolean isNewDate(int year, int month, int dayOfMonth) { - return (mCurrentDate.get(Calendar.YEAR) != year - || mCurrentDate.get(Calendar.MONTH) != dayOfMonth - || mCurrentDate.get(Calendar.DAY_OF_MONTH) != month); - } - - private void setDate(int year, int month, int dayOfMonth) { - mCurrentDate.set(year, month, dayOfMonth); - if (mCurrentDate.before(mMinDate)) { - mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis()); - } else if (mCurrentDate.after(mMaxDate)) { - mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis()); - } - } - - private void updateSpinners() { - // set the spinner ranges respecting the min and max dates - if (mCurrentDate.equals(mMinDate)) { - mDaySpinner.setMinValue(mCurrentDate.get(Calendar.DAY_OF_MONTH)); - mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH)); - mDaySpinner.setWrapSelectorWheel(false); - mMonthSpinner.setDisplayedValues(null); - mMonthSpinner.setMinValue(mCurrentDate.get(Calendar.MONTH)); - mMonthSpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.MONTH)); - mMonthSpinner.setWrapSelectorWheel(false); - } else if (mCurrentDate.equals(mMaxDate)) { - mDaySpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.DAY_OF_MONTH)); - mDaySpinner.setMaxValue(mCurrentDate.get(Calendar.DAY_OF_MONTH)); - mDaySpinner.setWrapSelectorWheel(false); - mMonthSpinner.setDisplayedValues(null); - mMonthSpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.MONTH)); - mMonthSpinner.setMaxValue(mCurrentDate.get(Calendar.MONTH)); - mMonthSpinner.setWrapSelectorWheel(false); - } else { - mDaySpinner.setMinValue(1); - mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH)); - mDaySpinner.setWrapSelectorWheel(true); - mMonthSpinner.setDisplayedValues(null); - mMonthSpinner.setMinValue(0); - mMonthSpinner.setMaxValue(11); - mMonthSpinner.setWrapSelectorWheel(true); } - // make sure the month names are a zero based array - // with the months in the month spinner - String[] displayedValues = Arrays.copyOfRange(mShortMonths, - mMonthSpinner.getMinValue(), mMonthSpinner.getMaxValue() + 1); - mMonthSpinner.setDisplayedValues(displayedValues); + @Override + public void onPopulateAccessibilityEvent(AccessibilityEvent event) { + final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR; + String selectedDateUtterance = DateUtils.formatDateTime(mContext, + mCurrentDate.getTimeInMillis(), flags); + event.getText().add(selectedDateUtterance); + } - // year spinner range does not change based on the current date - mYearSpinner.setMinValue(mMinDate.get(Calendar.YEAR)); - mYearSpinner.setMaxValue(mMaxDate.get(Calendar.YEAR)); - mYearSpinner.setWrapSelectorWheel(false); + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + event.setClassName(DatePicker.class.getName()); + } - // set the spinner values - mYearSpinner.setValue(mCurrentDate.get(Calendar.YEAR)); - mMonthSpinner.setValue(mCurrentDate.get(Calendar.MONTH)); - mDaySpinner.setValue(mCurrentDate.get(Calendar.DAY_OF_MONTH)); + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + info.setClassName(DatePicker.class.getName()); + } - if (usingNumericMonths()) { - mMonthSpinnerInput.setRawInputType(InputType.TYPE_CLASS_NUMBER); + /** + * Sets the current locale. + * + * @param locale The current locale. + */ + @Override + protected void setCurrentLocale(Locale locale) { + super.setCurrentLocale(locale); + + mTempDate = getCalendarForLocale(mTempDate, locale); + mMinDate = getCalendarForLocale(mMinDate, locale); + mMaxDate = getCalendarForLocale(mMaxDate, locale); + mCurrentDate = getCalendarForLocale(mCurrentDate, locale); + + mNumberOfMonths = mTempDate.getActualMaximum(Calendar.MONTH) + 1; + mShortMonths = new DateFormatSymbols().getShortMonths(); + + if (usingNumericMonths()) { + // We're in a locale where a date should either be all-numeric, or all-text. + // All-text would require custom NumberPicker formatters for day and year. + mShortMonths = new String[mNumberOfMonths]; + for (int i = 0; i < mNumberOfMonths; ++i) { + mShortMonths[i] = String.format("%d", i + 1); + } + } } - } - /** - * Updates the calendar view with the current date. - */ - private void updateCalendarView() { - mCalendarView.setDate(mCurrentDate.getTimeInMillis(), false, false); - } + /** + * Tests whether the current locale is one where there are no real month names, + * such as Chinese, Japanese, or Korean locales. + */ + private boolean usingNumericMonths() { + return Character.isDigit(mShortMonths[Calendar.JANUARY].charAt(0)); + } - /** - * @return The selected year. - */ - public int getYear() { - return mCurrentDate.get(Calendar.YEAR); - } + /** + * Gets a calendar for locale bootstrapped with the value of a given calendar. + * + * @param oldCalendar The old calendar. + * @param locale The locale. + */ + private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) { + if (oldCalendar == null) { + return Calendar.getInstance(locale); + } else { + final long currentTimeMillis = oldCalendar.getTimeInMillis(); + Calendar newCalendar = Calendar.getInstance(locale); + newCalendar.setTimeInMillis(currentTimeMillis); + return newCalendar; + } + } - /** - * @return The selected month. - */ - public int getMonth() { - return mCurrentDate.get(Calendar.MONTH); - } + /** + * Reorders the spinners according to the date format that is + * explicitly set by the user and if no such is set fall back + * to the current locale's default format. + */ + private void reorderSpinners() { + mSpinners.removeAllViews(); + // We use numeric spinners for year and day, but textual months. Ask icu4c what + // order the user's locale uses for that combination. http://b/7207103. + String pattern = ICU.getBestDateTimePattern("yyyyMMMdd", + Locale.getDefault().toString()); + char[] order = ICU.getDateFormatOrder(pattern); + final int spinnerCount = order.length; + for (int i = 0; i < spinnerCount; i++) { + switch (order[i]) { + case 'd': + mSpinners.addView(mDaySpinner); + setImeOptions(mDaySpinner, spinnerCount, i); + break; + case 'M': + mSpinners.addView(mMonthSpinner); + setImeOptions(mMonthSpinner, spinnerCount, i); + break; + case 'y': + mSpinners.addView(mYearSpinner); + setImeOptions(mYearSpinner, spinnerCount, i); + break; + default: + throw new IllegalArgumentException(Arrays.toString(order)); + } + } + } - /** - * @return The selected day of month. - */ - public int getDayOfMonth() { - return mCurrentDate.get(Calendar.DAY_OF_MONTH); - } + /** + * Parses the given <code>date</code> and in case of success sets the result + * to the <code>outDate</code>. + * + * @return True if the date was parsed. + */ + private boolean parseDate(String date, Calendar outDate) { + try { + outDate.setTime(mDateFormat.parse(date)); + return true; + } catch (ParseException e) { + Log.w(LOG_TAG, "Date: " + date + " not in format: " + DATE_FORMAT); + return false; + } + } - /** - * Notifies the listener, if such, for a change in the selected date. - */ - private void notifyDateChanged() { - sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); - if (mOnDateChangedListener != null) { - mOnDateChangedListener.onDateChanged(this, getYear(), getMonth(), getDayOfMonth()); + private boolean isNewDate(int year, int month, int dayOfMonth) { + return (mCurrentDate.get(Calendar.YEAR) != year + || mCurrentDate.get(Calendar.MONTH) != dayOfMonth + || mCurrentDate.get(Calendar.DAY_OF_MONTH) != month); } - } - /** - * Sets the IME options for a spinner based on its ordering. - * - * @param spinner The spinner. - * @param spinnerCount The total spinner count. - * @param spinnerIndex The index of the given spinner. - */ - private void setImeOptions(NumberPicker spinner, int spinnerCount, int spinnerIndex) { - final int imeOptions; - if (spinnerIndex < spinnerCount - 1) { - imeOptions = EditorInfo.IME_ACTION_NEXT; - } else { - imeOptions = EditorInfo.IME_ACTION_DONE; - } - TextView input = (TextView) spinner.findViewById(R.id.numberpicker_input); - input.setImeOptions(imeOptions); - } - - private void setContentDescriptions() { - // Day - trySetContentDescription(mDaySpinner, R.id.increment, - R.string.date_picker_increment_day_button); - trySetContentDescription(mDaySpinner, R.id.decrement, - R.string.date_picker_decrement_day_button); - // Month - trySetContentDescription(mMonthSpinner, R.id.increment, - R.string.date_picker_increment_month_button); - trySetContentDescription(mMonthSpinner, R.id.decrement, - R.string.date_picker_decrement_month_button); - // Year - trySetContentDescription(mYearSpinner, R.id.increment, - R.string.date_picker_increment_year_button); - trySetContentDescription(mYearSpinner, R.id.decrement, - R.string.date_picker_decrement_year_button); - } - - private void trySetContentDescription(View root, int viewId, int contDescResId) { - View target = root.findViewById(viewId); - if (target != null) { - target.setContentDescription(mContext.getString(contDescResId)); - } - } - - private void updateInputState() { - // Make sure that if the user changes the value and the IME is active - // for one of the inputs if this widget, the IME is closed. If the user - // changed the value via the IME and there is a next input the IME will - // be shown, otherwise the user chose another means of changing the - // value and having the IME up makes no sense. - InputMethodManager inputMethodManager = InputMethodManager.peekInstance(); - if (inputMethodManager != null) { - if (inputMethodManager.isActive(mYearSpinnerInput)) { - mYearSpinnerInput.clearFocus(); - inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); - } else if (inputMethodManager.isActive(mMonthSpinnerInput)) { - mMonthSpinnerInput.clearFocus(); - inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); - } else if (inputMethodManager.isActive(mDaySpinnerInput)) { - mDaySpinnerInput.clearFocus(); - inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); + private void setDate(int year, int month, int dayOfMonth) { + mCurrentDate.set(year, month, dayOfMonth); + if (mCurrentDate.before(mMinDate)) { + mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis()); + } else if (mCurrentDate.after(mMaxDate)) { + mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis()); + } + } + + private void updateSpinners() { + // set the spinner ranges respecting the min and max dates + if (mCurrentDate.equals(mMinDate)) { + mDaySpinner.setMinValue(mCurrentDate.get(Calendar.DAY_OF_MONTH)); + mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH)); + mDaySpinner.setWrapSelectorWheel(false); + mMonthSpinner.setDisplayedValues(null); + mMonthSpinner.setMinValue(mCurrentDate.get(Calendar.MONTH)); + mMonthSpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.MONTH)); + mMonthSpinner.setWrapSelectorWheel(false); + } else if (mCurrentDate.equals(mMaxDate)) { + mDaySpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.DAY_OF_MONTH)); + mDaySpinner.setMaxValue(mCurrentDate.get(Calendar.DAY_OF_MONTH)); + mDaySpinner.setWrapSelectorWheel(false); + mMonthSpinner.setDisplayedValues(null); + mMonthSpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.MONTH)); + mMonthSpinner.setMaxValue(mCurrentDate.get(Calendar.MONTH)); + mMonthSpinner.setWrapSelectorWheel(false); + } else { + mDaySpinner.setMinValue(1); + mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH)); + mDaySpinner.setWrapSelectorWheel(true); + mMonthSpinner.setDisplayedValues(null); + mMonthSpinner.setMinValue(0); + mMonthSpinner.setMaxValue(11); + mMonthSpinner.setWrapSelectorWheel(true); + } + + // make sure the month names are a zero based array + // with the months in the month spinner + String[] displayedValues = Arrays.copyOfRange(mShortMonths, + mMonthSpinner.getMinValue(), mMonthSpinner.getMaxValue() + 1); + mMonthSpinner.setDisplayedValues(displayedValues); + + // year spinner range does not change based on the current date + mYearSpinner.setMinValue(mMinDate.get(Calendar.YEAR)); + mYearSpinner.setMaxValue(mMaxDate.get(Calendar.YEAR)); + mYearSpinner.setWrapSelectorWheel(false); + + // set the spinner values + mYearSpinner.setValue(mCurrentDate.get(Calendar.YEAR)); + mMonthSpinner.setValue(mCurrentDate.get(Calendar.MONTH)); + mDaySpinner.setValue(mCurrentDate.get(Calendar.DAY_OF_MONTH)); + + if (usingNumericMonths()) { + mMonthSpinnerInput.setRawInputType(InputType.TYPE_CLASS_NUMBER); + } + } + + /** + * Updates the calendar view with the current date. + */ + private void updateCalendarView() { + mCalendarView.setDate(mCurrentDate.getTimeInMillis(), false, false); + } + + + /** + * Notifies the listener, if such, for a change in the selected date. + */ + private void notifyDateChanged() { + mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); + if (mOnDateChangedListener != null) { + mOnDateChangedListener.onDateChanged(mDelegator, getYear(), getMonth(), + getDayOfMonth()); + } + } + + /** + * Sets the IME options for a spinner based on its ordering. + * + * @param spinner The spinner. + * @param spinnerCount The total spinner count. + * @param spinnerIndex The index of the given spinner. + */ + private void setImeOptions(NumberPicker spinner, int spinnerCount, int spinnerIndex) { + final int imeOptions; + if (spinnerIndex < spinnerCount - 1) { + imeOptions = EditorInfo.IME_ACTION_NEXT; + } else { + imeOptions = EditorInfo.IME_ACTION_DONE; + } + TextView input = (TextView) spinner.findViewById(R.id.numberpicker_input); + input.setImeOptions(imeOptions); + } + + private void setContentDescriptions() { + // Day + trySetContentDescription(mDaySpinner, R.id.increment, + R.string.date_picker_increment_day_button); + trySetContentDescription(mDaySpinner, R.id.decrement, + R.string.date_picker_decrement_day_button); + // Month + trySetContentDescription(mMonthSpinner, R.id.increment, + R.string.date_picker_increment_month_button); + trySetContentDescription(mMonthSpinner, R.id.decrement, + R.string.date_picker_decrement_month_button); + // Year + trySetContentDescription(mYearSpinner, R.id.increment, + R.string.date_picker_increment_year_button); + trySetContentDescription(mYearSpinner, R.id.decrement, + R.string.date_picker_decrement_year_button); + } + + private void trySetContentDescription(View root, int viewId, int contDescResId) { + View target = root.findViewById(viewId); + if (target != null) { + target.setContentDescription(mContext.getString(contDescResId)); + } + } + + private void updateInputState() { + // Make sure that if the user changes the value and the IME is active + // for one of the inputs if this widget, the IME is closed. If the user + // changed the value via the IME and there is a next input the IME will + // be shown, otherwise the user chose another means of changing the + // value and having the IME up makes no sense. + InputMethodManager inputMethodManager = InputMethodManager.peekInstance(); + if (inputMethodManager != null) { + if (inputMethodManager.isActive(mYearSpinnerInput)) { + mYearSpinnerInput.clearFocus(); + inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0); + } else if (inputMethodManager.isActive(mMonthSpinnerInput)) { + mMonthSpinnerInput.clearFocus(); + inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0); + } else if (inputMethodManager.isActive(mDaySpinnerInput)) { + mDaySpinnerInput.clearFocus(); + inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0); + } } } } diff --git a/core/java/android/widget/DateTimeView.java b/core/java/android/widget/DateTimeView.java index af6bbcb..45d1403 100644 --- a/core/java/android/widget/DateTimeView.java +++ b/core/java/android/widget/DateTimeView.java @@ -27,12 +27,9 @@ import android.text.format.Time; import android.util.AttributeSet; import android.util.Log; import android.provider.Settings; -import android.provider.Settings.SettingNotFoundException; import android.widget.TextView; import android.widget.RemoteViews.RemoteView; -import com.android.internal.R; - import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; diff --git a/core/java/android/widget/DialerFilter.java b/core/java/android/widget/DialerFilter.java index 20bc114..78786e1 100644 --- a/core/java/android/widget/DialerFilter.java +++ b/core/java/android/widget/DialerFilter.java @@ -28,8 +28,6 @@ import android.text.method.DialerKeyListener; import android.text.method.KeyListener; import android.text.method.TextKeyListener; import android.util.AttributeSet; -import android.util.Log; -import android.view.KeyCharacterMap; import android.view.View; import android.graphics.Rect; diff --git a/core/java/android/widget/EdgeEffect.java b/core/java/android/widget/EdgeEffect.java index 30752e0..fa37443 100644 --- a/core/java/android/widget/EdgeEffect.java +++ b/core/java/android/widget/EdgeEffect.java @@ -136,8 +136,8 @@ public class EdgeEffect { */ public EdgeEffect(Context context) { final Resources res = context.getResources(); - mEdge = res.getDrawable(R.drawable.overscroll_edge); - mGlow = res.getDrawable(R.drawable.overscroll_glow); + mEdge = context.getDrawable(R.drawable.overscroll_edge); + mGlow = context.getDrawable(R.drawable.overscroll_glow); mEdgeHeight = mEdge.getIntrinsicHeight(); mGlowHeight = mGlow.getIntrinsicHeight(); diff --git a/core/java/android/widget/EditText.java b/core/java/android/widget/EditText.java index 57e51c2..a8ff562 100644 --- a/core/java/android/widget/EditText.java +++ b/core/java/android/widget/EditText.java @@ -17,6 +17,7 @@ package android.widget; import android.content.Context; +import android.os.Bundle; import android.text.Editable; import android.text.Selection; import android.text.Spannable; @@ -56,8 +57,12 @@ public class EditText extends TextView { this(context, attrs, com.android.internal.R.attr.editTextStyle); } - public EditText(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public EditText(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public EditText(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); } @Override @@ -128,4 +133,22 @@ public class EditText extends TextView { super.onInitializeAccessibilityNodeInfo(info); info.setClassName(EditText.class.getName()); } + + @Override + public boolean performAccessibilityAction(int action, Bundle arguments) { + switch (action) { + case AccessibilityNodeInfo.ACTION_SET_TEXT: { + CharSequence text = (arguments != null) ? arguments.getCharSequence( + AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE) : null; + setText(text); + if (text != null && text.length() > 0) { + setSelection(text.length()); + } + return true; + } + default: { + return super.performAccessibilityAction(action, arguments); + } + } + } } diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index 748af7b..b0a4e24 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -23,7 +23,9 @@ import android.os.Parcel; import android.os.Parcelable; import android.text.InputFilter; import android.text.SpannableString; + import com.android.internal.util.ArrayUtils; +import com.android.internal.util.GrowingArrayUtils; import com.android.internal.widget.EditableInputConnection; import android.R; @@ -45,8 +47,6 @@ import android.graphics.drawable.Drawable; import android.inputmethodservice.ExtractEditText; import android.os.Bundle; import android.os.Handler; -import android.os.Message; -import android.os.Messenger; import android.os.SystemClock; import android.provider.Settings; import android.text.DynamicLayout; @@ -75,10 +75,11 @@ import android.util.DisplayMetrics; import android.util.Log; import android.view.ActionMode; import android.view.ActionMode.Callback; -import android.view.DisplayList; +import android.view.RenderNode; import android.view.DragEvent; import android.view.Gravity; import android.view.HardwareCanvas; +import android.view.HardwareRenderer; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; @@ -137,7 +138,16 @@ public class Editor { InputContentType mInputContentType; InputMethodState mInputMethodState; - DisplayList[] mTextDisplayLists; + private static class TextDisplayList { + RenderNode displayList; + boolean isDirty; + public TextDisplayList(String name) { + isDirty = true; + displayList = RenderNode.create(name); + } + boolean needsRecord() { return isDirty || !displayList.isValid(); } + } + TextDisplayList[] mTextDisplayLists; boolean mFrozenWithFocus; boolean mSelectionMoved; @@ -262,7 +272,7 @@ public class Editor { mTextView.removeCallbacks(mShowSuggestionRunnable); } - invalidateTextDisplayList(); + destroyDisplayListsData(); if (mSpellChecker != null) { mSpellChecker.closeSession(); @@ -277,6 +287,18 @@ public class Editor { mTemporaryDetach = false; } + private void destroyDisplayListsData() { + if (mTextDisplayLists != null) { + for (int i = 0; i < mTextDisplayLists.length; i++) { + RenderNode displayList = mTextDisplayLists[i] != null + ? mTextDisplayLists[i].displayList : null; + if (displayList != null && displayList.isValid()) { + displayList.destroyDisplayListData(); + } + } + } + } + private void showError() { if (mTextView.getWindowToken() == null) { mShowErrorAfterAttach = true; @@ -1319,7 +1341,7 @@ public class Editor { if (layout instanceof DynamicLayout) { if (mTextDisplayLists == null) { - mTextDisplayLists = new DisplayList[ArrayUtils.idealObjectArraySize(0)]; + mTextDisplayLists = ArrayUtils.emptyArray(TextDisplayList.class); } DynamicLayout dynamicLayout = (DynamicLayout) layout; @@ -1343,15 +1365,13 @@ public class Editor { searchStartIndex = blockIndex + 1; } - DisplayList blockDisplayList = mTextDisplayLists[blockIndex]; - if (blockDisplayList == null) { - blockDisplayList = mTextDisplayLists[blockIndex] = - mTextView.getHardwareRenderer().createDisplayList("Text " + blockIndex); - } else { - if (blockIsInvalid) blockDisplayList.clear(); + if (mTextDisplayLists[blockIndex] == null) { + mTextDisplayLists[blockIndex] = + new TextDisplayList("Text " + blockIndex); } - final boolean blockDisplayListIsInvalid = !blockDisplayList.isValid(); + final boolean blockDisplayListIsInvalid = mTextDisplayLists[blockIndex].needsRecord(); + RenderNode blockDisplayList = mTextDisplayLists[blockIndex].displayList; if (i >= indexFirstChangedBlock || blockDisplayListIsInvalid) { final int blockBeginLine = endOfPreviousBlock + 1; final int top = layout.getLineTop(blockBeginLine); @@ -1381,7 +1401,7 @@ public class Editor { // No need to untranslate, previous context is popped after // drawDisplayList } finally { - blockDisplayList.end(); + blockDisplayList.end(hardwareCanvas); // Same as drawDisplayList below, handled by our TextView's parent blockDisplayList.setClipToBounds(false); } @@ -1421,10 +1441,7 @@ public class Editor { } // No available index found, the pool has to grow - int newSize = ArrayUtils.idealIntArraySize(length + 1); - DisplayList[] displayLists = new DisplayList[newSize]; - System.arraycopy(mTextDisplayLists, 0, displayLists, 0, length); - mTextDisplayLists = displayLists; + mTextDisplayLists = GrowingArrayUtils.append(mTextDisplayLists, length, null); return length; } @@ -1461,7 +1478,7 @@ public class Editor { while (i < numberOfBlocks) { final int blockIndex = blockIndices[i]; if (blockIndex != DynamicLayout.INVALID_BLOCK_INDEX) { - mTextDisplayLists[blockIndex].clear(); + mTextDisplayLists[blockIndex].isDirty = true; } if (blockEndLines[i] >= lastLine) break; i++; @@ -1472,7 +1489,7 @@ public class Editor { void invalidateTextDisplayList() { if (mTextDisplayLists != null) { for (int i = 0; i < mTextDisplayLists.length; i++) { - if (mTextDisplayLists[i] != null) mTextDisplayLists[i].clear(); + if (mTextDisplayLists[i] != null) mTextDisplayLists[i].isDirty = true; } } } @@ -1681,7 +1698,7 @@ public class Editor { private void updateCursorPosition(int cursorIndex, int top, int bottom, float horizontal) { if (mCursorDrawable[cursorIndex] == null) - mCursorDrawable[cursorIndex] = mTextView.getResources().getDrawable( + mCursorDrawable[cursorIndex] = mTextView.getContext().getDrawable( mTextView.mCursorDrawableRes); if (mTempRect == null) mTempRect = new Rect(); @@ -2321,8 +2338,8 @@ public class Editor { private final HashMap<SuggestionSpan, Integer> mSpansLengths; private class CustomPopupWindow extends PopupWindow { - public CustomPopupWindow(Context context, int defStyle) { - super(context, null, defStyle); + public CustomPopupWindow(Context context, int defStyleAttr) { + super(context, null, defStyleAttr); } @Override @@ -2971,7 +2988,7 @@ public class Editor { positionY += mContentView.getMeasuredHeight(); // Assumes insertion and selection handles share the same height - final Drawable handle = mTextView.getResources().getDrawable( + final Drawable handle = mTextView.getContext().getDrawable( mTextView.mTextSelectHandleRes); positionY += handle.getIntrinsicHeight(); } @@ -3548,7 +3565,7 @@ public class Editor { private InsertionHandleView getHandle() { if (mSelectHandleCenter == null) { - mSelectHandleCenter = mTextView.getResources().getDrawable( + mSelectHandleCenter = mTextView.getContext().getDrawable( mTextView.mTextSelectHandleRes); } if (mHandle == null) { @@ -3594,11 +3611,11 @@ public class Editor { private void initDrawables() { if (mSelectHandleLeft == null) { - mSelectHandleLeft = mTextView.getContext().getResources().getDrawable( + mSelectHandleLeft = mTextView.getContext().getDrawable( mTextView.mTextSelectHandleLeftRes); } if (mSelectHandleRight == null) { - mSelectHandleRight = mTextView.getContext().getResources().getDrawable( + mSelectHandleRight = mTextView.getContext().getDrawable( mTextView.mTextSelectHandleRightRes); } } diff --git a/core/java/android/widget/ExpandableListView.java b/core/java/android/widget/ExpandableListView.java index 7b81aa8..70089e0 100644 --- a/core/java/android/widget/ExpandableListView.java +++ b/core/java/android/widget/ExpandableListView.java @@ -227,12 +227,16 @@ public class ExpandableListView extends ListView { this(context, attrs, com.android.internal.R.attr.expandableListViewStyle); } - public ExpandableListView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public ExpandableListView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ExpandableListView( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = - context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.ExpandableListView, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.ExpandableListView, defStyleAttr, defStyleRes); mGroupIndicator = a.getDrawable( com.android.internal.R.styleable.ExpandableListView_groupIndicator); diff --git a/core/java/android/widget/FastScroller.java b/core/java/android/widget/FastScroller.java index 4379bf6..c0961fd 100644 --- a/core/java/android/widget/FastScroller.java +++ b/core/java/android/widget/FastScroller.java @@ -24,11 +24,11 @@ import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.content.Context; import android.content.res.ColorStateList; -import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Build; +import android.os.SystemClock; import android.text.TextUtils; import android.text.TextUtils.TruncateAt; import android.util.IntProperty; @@ -43,7 +43,7 @@ import android.view.ViewConfiguration; import android.view.ViewGroup.LayoutParams; import android.view.ViewGroupOverlay; import android.widget.AbsListView.OnScrollListener; -import com.android.internal.R; +import android.widget.ImageView.ScaleType; /** * Helper class for AbsListView to draw and control the Fast Scroll thumb @@ -76,24 +76,6 @@ class FastScroller { /** Scroll thumb and preview being dragged by user. */ private static final int STATE_DRAGGING = 2; - /** Styleable attributes. */ - private static final int[] ATTRS = new int[] { - android.R.attr.fastScrollTextColor, - android.R.attr.fastScrollThumbDrawable, - android.R.attr.fastScrollTrackDrawable, - android.R.attr.fastScrollPreviewBackgroundLeft, - android.R.attr.fastScrollPreviewBackgroundRight, - android.R.attr.fastScrollOverlayPosition - }; - - // Styleable attribute indices. - private static final int TEXT_COLOR = 0; - private static final int THUMB_DRAWABLE = 1; - private static final int TRACK_DRAWABLE = 2; - private static final int PREVIEW_BACKGROUND_LEFT = 3; - private static final int PREVIEW_BACKGROUND_RIGHT = 4; - private static final int OVERLAY_POSITION = 5; - // Positions for preview image and text. private static final int OVERLAY_FLOATING = 0; private static final int OVERLAY_AT_THUMB = 1; @@ -115,7 +97,7 @@ class FastScroller { private final TextView mSecondaryText; private final ImageView mThumbImage; private final ImageView mTrackImage; - private final ImageView mPreviewImage; + private final View mPreviewImage; /** * Preview image resource IDs for left- and right-aligned layouts. See @@ -127,13 +109,25 @@ class FastScroller { * Padding in pixels around the preview text. Applied as layout margins to * the preview text and padding to the preview image. */ - private final int mPreviewPadding; + private int mPreviewPadding; + + private int mPreviewMinWidth; + private int mPreviewMinHeight; + private int mThumbMinWidth; + private int mThumbMinHeight; - /** Whether there is a track image to display. */ - private final boolean mHasTrackImage; + /** Theme-specified text size. Used only if text appearance is not set. */ + private float mTextSize; + + /** Theme-specified text color. Used only if text appearance is not set. */ + private ColorStateList mTextColor; + + private Drawable mThumbDrawable; + private Drawable mTrackDrawable; + private int mTextAppearance; /** Total width of decorations. */ - private final int mWidth; + private int mWidth; /** Set containing decoration transition animations. */ private AnimatorSet mDecorAnimation; @@ -180,7 +174,7 @@ class FastScroller { /** Whether the preview image is visible. */ private boolean mShowingPreview; - private BaseAdapter mListAdapter; + private Adapter mListAdapter; private SectionIndexer mSectionIndexer; /** Whether decorations should be laid out from right to left. */ @@ -208,22 +202,9 @@ class FastScroller { private boolean mMatchDragPosition; private float mInitialTouchY; - private boolean mHasPendingDrag; + private long mPendingDrag = -1; private int mScaledTouchSlop; - private final Runnable mDeferStartDrag = new Runnable() { - @Override - public void run() { - if (mList.isAttachedToWindow()) { - beginDrag(); - - final float pos = getPosFromMotionEvent(mInitialTouchY); - scrollTo(pos); - } - - mHasPendingDrag = false; - } - }; private int mOldItemCount; private int mOldChildCount; @@ -247,92 +228,145 @@ class FastScroller { } }; - public FastScroller(AbsListView listView) { + public FastScroller(AbsListView listView, int styleResId) { mList = listView; - mOverlay = listView.getOverlay(); mOldItemCount = listView.getCount(); mOldChildCount = listView.getChildCount(); final Context context = listView.getContext(); mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + mScrollBarStyle = listView.getScrollBarStyle(); - final Resources res = context.getResources(); - final TypedArray ta = context.getTheme().obtainStyledAttributes(ATTRS); + mScrollCompleted = true; + mState = STATE_VISIBLE; + mMatchDragPosition = + context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB; + + mTrackImage = new ImageView(context); + mTrackImage.setScaleType(ScaleType.FIT_XY); + mThumbImage = new ImageView(context); + mThumbImage.setScaleType(ScaleType.FIT_XY); + mPreviewImage = new View(context); + mPreviewImage.setAlpha(0f); + + mPrimaryText = createPreviewTextView(context); + mSecondaryText = createPreviewTextView(context); - final ImageView trackImage = new ImageView(context); - mTrackImage = trackImage; + setStyle(styleResId); + final ViewGroupOverlay overlay = listView.getOverlay(); + mOverlay = overlay; + overlay.add(mTrackImage); + overlay.add(mThumbImage); + overlay.add(mPreviewImage); + overlay.add(mPrimaryText); + overlay.add(mSecondaryText); + + getSectionsFromIndexer(); updateLongList(mOldChildCount, mOldItemCount); + setScrollbarPosition(listView.getVerticalScrollbarPosition()); + postAutoHide(); + } + + private void updateAppearance() { + final Context context = mList.getContext(); int width = 0; // Add track to overlay if it has an image. - final Drawable trackDrawable = ta.getDrawable(TRACK_DRAWABLE); - if (trackDrawable != null) { - mHasTrackImage = true; - trackImage.setBackground(trackDrawable); - mOverlay.add(trackImage); - width = Math.max(width, trackDrawable.getIntrinsicWidth()); - } else { - mHasTrackImage = false; + mTrackImage.setImageDrawable(mTrackDrawable); + if (mTrackDrawable != null) { + width = Math.max(width, mTrackDrawable.getIntrinsicWidth()); } - final ImageView thumbImage = new ImageView(context); - mThumbImage = thumbImage; - // Add thumb to overlay if it has an image. - final Drawable thumbDrawable = ta.getDrawable(THUMB_DRAWABLE); - if (thumbDrawable != null) { - thumbImage.setImageDrawable(thumbDrawable); - mOverlay.add(thumbImage); - width = Math.max(width, thumbDrawable.getIntrinsicWidth()); + mThumbImage.setImageDrawable(mThumbDrawable); + mThumbImage.setMinimumWidth(mThumbMinWidth); + mThumbImage.setMinimumHeight(mThumbMinHeight); + if (mThumbDrawable != null) { + width = Math.max(width, mThumbDrawable.getIntrinsicWidth()); } - // If necessary, apply minimum thumb width and height. - if (thumbDrawable.getIntrinsicWidth() <= 0 || thumbDrawable.getIntrinsicHeight() <= 0) { - final int minWidth = res.getDimensionPixelSize(R.dimen.fastscroll_thumb_width); - thumbImage.setMinimumWidth(minWidth); - thumbImage.setMinimumHeight( - res.getDimensionPixelSize(R.dimen.fastscroll_thumb_height)); - width = Math.max(width, minWidth); - } + // Account for minimum thumb width. + mWidth = Math.max(width, mThumbMinWidth); - mWidth = width; + mPreviewImage.setMinimumWidth(mPreviewMinWidth); + mPreviewImage.setMinimumHeight(mPreviewMinHeight); - final int previewSize = res.getDimensionPixelSize(R.dimen.fastscroll_overlay_size); - mPreviewImage = new ImageView(context); - mPreviewImage.setMinimumWidth(previewSize); - mPreviewImage.setMinimumHeight(previewSize); - mPreviewImage.setAlpha(0f); - mOverlay.add(mPreviewImage); + if (mTextAppearance != 0) { + mPrimaryText.setTextAppearance(context, mTextAppearance); + mSecondaryText.setTextAppearance(context, mTextAppearance); + } - mPreviewPadding = res.getDimensionPixelSize(R.dimen.fastscroll_overlay_padding); + if (mTextColor != null) { + mPrimaryText.setTextColor(mTextColor); + mSecondaryText.setTextColor(mTextColor); + } + + if (mTextSize > 0) { + mPrimaryText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize); + mSecondaryText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize); + } - final int textMinSize = Math.max(0, previewSize - mPreviewPadding); - mPrimaryText = createPreviewTextView(context, ta); + final int textMinSize = Math.max(0, mPreviewMinHeight); mPrimaryText.setMinimumWidth(textMinSize); mPrimaryText.setMinimumHeight(textMinSize); - mOverlay.add(mPrimaryText); - mSecondaryText = createPreviewTextView(context, ta); mSecondaryText.setMinimumWidth(textMinSize); mSecondaryText.setMinimumHeight(textMinSize); - mOverlay.add(mSecondaryText); - mPreviewResId[PREVIEW_LEFT] = ta.getResourceId(PREVIEW_BACKGROUND_LEFT, 0); - mPreviewResId[PREVIEW_RIGHT] = ta.getResourceId(PREVIEW_BACKGROUND_RIGHT, 0); - mOverlayPosition = ta.getInt(OVERLAY_POSITION, OVERLAY_FLOATING); - ta.recycle(); + refreshDrawablePressedState(); + } - mScrollBarStyle = listView.getScrollBarStyle(); - mScrollCompleted = true; - mState = STATE_VISIBLE; - mMatchDragPosition = context.getApplicationInfo().targetSdkVersion - >= Build.VERSION_CODES.HONEYCOMB; + public void setStyle(int resId) { + final Context context = mList.getContext(); + final TypedArray ta = context.obtainStyledAttributes(null, + com.android.internal.R.styleable.FastScroll, android.R.attr.fastScrollStyle, resId); + final int N = ta.getIndexCount(); + for (int i = 0; i < N; i++) { + final int index = ta.getIndex(i); + switch (index) { + case com.android.internal.R.styleable.FastScroll_position: + mOverlayPosition = ta.getInt(index, OVERLAY_FLOATING); + break; + case com.android.internal.R.styleable.FastScroll_backgroundLeft: + mPreviewResId[PREVIEW_LEFT] = ta.getResourceId(index, 0); + break; + case com.android.internal.R.styleable.FastScroll_backgroundRight: + mPreviewResId[PREVIEW_RIGHT] = ta.getResourceId(index, 0); + break; + case com.android.internal.R.styleable.FastScroll_thumbDrawable: + mThumbDrawable = ta.getDrawable(index); + break; + case com.android.internal.R.styleable.FastScroll_trackDrawable: + mTrackDrawable = ta.getDrawable(index); + break; + case com.android.internal.R.styleable.FastScroll_textAppearance: + mTextAppearance = ta.getResourceId(index, 0); + break; + case com.android.internal.R.styleable.FastScroll_textColor: + mTextColor = ta.getColorStateList(index); + break; + case com.android.internal.R.styleable.FastScroll_textSize: + mTextSize = ta.getDimensionPixelSize(index, 0); + break; + case com.android.internal.R.styleable.FastScroll_minWidth: + mPreviewMinWidth = ta.getDimensionPixelSize(index, 0); + break; + case com.android.internal.R.styleable.FastScroll_minHeight: + mPreviewMinHeight = ta.getDimensionPixelSize(index, 0); + break; + case com.android.internal.R.styleable.FastScroll_thumbMinWidth: + mThumbMinWidth = ta.getDimensionPixelSize(index, 0); + break; + case com.android.internal.R.styleable.FastScroll_thumbMinHeight: + mThumbMinHeight = ta.getDimensionPixelSize(index, 0); + break; + case com.android.internal.R.styleable.FastScroll_padding: + mPreviewPadding = ta.getDimensionPixelSize(index, 0); + break; + } + } - getSectionsFromIndexer(); - refreshDrawablePressedState(); - updateLongList(listView.getChildCount(), listView.getCount()); - setScrollbarPosition(mList.getVerticalScrollbarPosition()); - postAutoHide(); + updateAppearance(); } /** @@ -353,7 +387,7 @@ class FastScroller { if (mEnabled != enabled) { mEnabled = enabled; - onStateDependencyChanged(); + onStateDependencyChanged(true); } } @@ -371,7 +405,7 @@ class FastScroller { if (mAlwaysShow != alwaysShow) { mAlwaysShow = alwaysShow; - onStateDependencyChanged(); + onStateDependencyChanged(false); } } @@ -385,13 +419,18 @@ class FastScroller { /** * Called when one of the variables affecting enabled state changes. + * + * @param peekIfEnabled whether the thumb should peek, if enabled */ - private void onStateDependencyChanged() { + private void onStateDependencyChanged(boolean peekIfEnabled) { if (isEnabled()) { if (isAlwaysShowEnabled()) { setState(STATE_VISIBLE); } else if (mState == STATE_VISIBLE) { postAutoHide(); + } else if (peekIfEnabled) { + setState(STATE_VISIBLE); + postAutoHide(); } } else { stop(); @@ -470,24 +509,18 @@ class FastScroller { if (mLongList != longList) { mLongList = longList; - onStateDependencyChanged(); + onStateDependencyChanged(false); } } /** * Creates a view into which preview text can be placed. */ - private TextView createPreviewTextView(Context context, TypedArray ta) { + private TextView createPreviewTextView(Context context) { final LayoutParams params = new LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); - final Resources res = context.getResources(); - final int minSize = res.getDimensionPixelSize(R.dimen.fastscroll_overlay_size); - final ColorStateList textColor = ta.getColorStateList(TEXT_COLOR); - final float textSize = res.getDimensionPixelSize(R.dimen.fastscroll_overlay_text_size); final TextView textView = new TextView(context); textView.setLayoutParams(params); - textView.setTextColor(textColor); - textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); textView.setSingleLine(true); textView.setEllipsize(TruncateAt.MIDDLE); textView.setGravity(Gravity.CENTER); @@ -611,7 +644,7 @@ class FastScroller { view.measure(widthMeasureSpec, heightMeasureSpec); // Align to the left or right. - final int width = view.getMeasuredWidth(); + final int width = Math.min(adjMaxWidth, view.getMeasuredWidth()); final int left; final int right; if (mLayoutFromRight) { @@ -872,15 +905,15 @@ class FastScroller { .getAdapter(); if (expAdapter instanceof SectionIndexer) { mSectionIndexer = (SectionIndexer) expAdapter; - mListAdapter = (BaseAdapter) adapter; + mListAdapter = adapter; mSections = mSectionIndexer.getSections(); } } else if (adapter instanceof SectionIndexer) { - mListAdapter = (BaseAdapter) adapter; + mListAdapter = adapter; mSectionIndexer = (SectionIndexer) adapter; mSections = mSectionIndexer.getSections(); } else { - mListAdapter = (BaseAdapter) adapter; + mListAdapter = adapter; mSections = null; } } @@ -1028,7 +1061,7 @@ class FastScroller { } final Rect bounds = mTempBounds; - final ImageView preview = mPreviewImage; + final View preview = mPreviewImage; final TextView showing; final TextView target; if (mShowingPrimary) { @@ -1054,10 +1087,10 @@ class FastScroller { hideShowing.addListener(mSwitchPrimaryListener); // Apply preview image padding and animate bounds, if necessary. - bounds.left -= mPreviewImage.getPaddingLeft(); - bounds.top -= mPreviewImage.getPaddingTop(); - bounds.right += mPreviewImage.getPaddingRight(); - bounds.bottom += mPreviewImage.getPaddingBottom(); + bounds.left -= preview.getPaddingLeft(); + bounds.top -= preview.getPaddingTop(); + bounds.right += preview.getPaddingRight(); + bounds.bottom += preview.getPaddingBottom(); final Animator resizePreview = animateBounds(preview, bounds); resizePreview.setDuration(DURATION_RESIZE); @@ -1105,8 +1138,8 @@ class FastScroller { final int top = container.top; final int bottom = container.bottom; - final ImageView trackImage = mTrackImage; - final ImageView thumbImage = mThumbImage; + final View trackImage = mTrackImage; + final View thumbImage = mThumbImage; final float min = trackImage.getTop(); final float max = trackImage.getBottom(); final float offset = min; @@ -1117,7 +1150,7 @@ class FastScroller { final float previewPos = mOverlayPosition == OVERLAY_AT_THUMB ? thumbMiddle : 0; // Center the preview on the thumb, constrained to the list bounds. - final ImageView previewImage = mPreviewImage; + final View previewImage = mPreviewImage; final float previewHalfHeight = previewImage.getHeight() / 2f; final float minP = top + previewHalfHeight; final float maxP = bottom - previewHalfHeight; @@ -1130,11 +1163,7 @@ class FastScroller { } private float getPosFromMotionEvent(float y) { - final Rect container = mContainerRect; - final int top = container.top; - final int bottom = container.bottom; - - final ImageView trackImage = mTrackImage; + final View trackImage = mTrackImage; final float min = trackImage.getTop(); final float max = trackImage.getBottom(); final float offset = min; @@ -1235,8 +1264,7 @@ class FastScroller { * @see #startPendingDrag() */ private void cancelPendingDrag() { - mList.removeCallbacks(mDeferStartDrag); - mHasPendingDrag = false; + mPendingDrag = -1; } /** @@ -1244,11 +1272,12 @@ class FastScroller { * scrolling, rather than tapping. */ private void startPendingDrag() { - mHasPendingDrag = true; - mList.postDelayed(mDeferStartDrag, TAP_TIMEOUT); + mPendingDrag = SystemClock.uptimeMillis() + TAP_TIMEOUT; } private void beginDrag() { + mPendingDrag = -1; + setState(STATE_DRAGGING); if (mListAdapter == null && mList != null) { @@ -1288,6 +1317,13 @@ class FastScroller { case MotionEvent.ACTION_MOVE: if (!isPointInside(ev.getX(), ev.getY())) { cancelPendingDrag(); + } else if (mPendingDrag >= 0 && mPendingDrag <= SystemClock.uptimeMillis()) { + beginDrag(); + + final float pos = getPosFromMotionEvent(mInitialTouchY); + scrollTo(pos); + + return onTouchEvent(ev); } break; case MotionEvent.ACTION_UP: @@ -1322,7 +1358,7 @@ class FastScroller { switch (me.getActionMasked()) { case MotionEvent.ACTION_UP: { - if (mHasPendingDrag) { + if (mPendingDrag >= 0) { // Allow a tap to scroll. beginDrag(); @@ -1330,7 +1366,6 @@ class FastScroller { setThumbPos(pos); scrollTo(pos); - cancelPendingDrag(); // Will hit the STATE_DRAGGING check below } @@ -1351,20 +1386,9 @@ class FastScroller { } break; case MotionEvent.ACTION_MOVE: { - if (mHasPendingDrag && Math.abs(me.getY() - mInitialTouchY) > mScaledTouchSlop) { - setState(STATE_DRAGGING); - - if (mListAdapter == null && mList != null) { - getSectionsFromIndexer(); - } - - if (mList != null) { - mList.requestDisallowInterceptTouchEvent(true); - mList.reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); - } + if (mPendingDrag >= 0 && Math.abs(me.getY() - mInitialTouchY) > mScaledTouchSlop) { + beginDrag(); - cancelFling(); - cancelPendingDrag(); // Will hit the STATE_DRAGGING check below } @@ -1401,7 +1425,7 @@ class FastScroller { * @return Whether the coordinate is inside the scroller's activation area. */ private boolean isPointInside(float x, float y) { - return isPointInsideX(x) && (mHasTrackImage || isPointInsideY(y)); + return isPointInsideX(x) && (mTrackDrawable != null || isPointInsideY(y)); } private boolean isPointInsideX(float x) { diff --git a/core/java/android/widget/FrameLayout.java b/core/java/android/widget/FrameLayout.java index d9d4ad7..b029328 100644 --- a/core/java/android/widget/FrameLayout.java +++ b/core/java/android/widget/FrameLayout.java @@ -97,11 +97,15 @@ public class FrameLayout extends ViewGroup { this(context, attrs, 0); } - public FrameLayout(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public FrameLayout(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public FrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.FrameLayout, - defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.FrameLayout, defStyleAttr, defStyleRes); mForegroundGravity = a.getInt( com.android.internal.R.styleable.FrameLayout_foregroundGravity, mForegroundGravity); diff --git a/core/java/android/widget/Gallery.java b/core/java/android/widget/Gallery.java index 6dd93a7..f7c839f 100644 --- a/core/java/android/widget/Gallery.java +++ b/core/java/android/widget/Gallery.java @@ -196,14 +196,18 @@ public class Gallery extends AbsSpinner implements GestureDetector.OnGestureList this(context, attrs, R.attr.galleryStyle); } - public Gallery(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public Gallery(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public Gallery(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); mGestureDetector = new GestureDetector(context, this); mGestureDetector.setIsLongpressEnabled(true); - - TypedArray a = context.obtainStyledAttributes( - attrs, com.android.internal.R.styleable.Gallery, defStyle, 0); + + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.Gallery, defStyleAttr, defStyleRes); int index = a.getInt(com.android.internal.R.styleable.Gallery_gravity, -1); if (index >= 0) { @@ -1228,7 +1232,7 @@ public class Gallery extends AbsSpinner implements GestureDetector.OnGestureList @Override public boolean onKeyUp(int keyCode, KeyEvent event) { - if (event.isConfirmKey()) { + if (KeyEvent.isConfirmKey(keyCode)) { if (mReceivedInvokeKeyDown) { if (mItemCount > 0) { dispatchPress(mSelectedChild); diff --git a/core/java/android/widget/GridLayout.java b/core/java/android/widget/GridLayout.java index 54cc3f4..8511601 100644 --- a/core/java/android/widget/GridLayout.java +++ b/core/java/android/widget/GridLayout.java @@ -16,6 +16,7 @@ package android.widget; +import android.annotation.IntDef; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; @@ -35,6 +36,8 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.widget.RemoteViews.RemoteView; import com.android.internal.R; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Arrays; @@ -165,6 +168,11 @@ public class GridLayout extends ViewGroup { // Public constants + /** @hide */ + @IntDef({HORIZONTAL, VERTICAL}) + @Retention(RetentionPolicy.SOURCE) + public @interface Orientation {} + /** * The horizontal orientation. */ @@ -186,6 +194,11 @@ public class GridLayout extends ViewGroup { */ public static final int UNDEFINED = Integer.MIN_VALUE; + /** @hide */ + @IntDef({ALIGN_BOUNDS, ALIGN_MARGINS}) + @Retention(RetentionPolicy.SOURCE) + public @interface AlignmentMode {} + /** * This constant is an {@link #setAlignmentMode(int) alignmentMode}. * When the {@code alignmentMode} is set to {@link #ALIGN_BOUNDS}, alignment @@ -262,13 +275,23 @@ public class GridLayout extends ViewGroup { // Constructors - /** - * {@inheritDoc} - */ - public GridLayout(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public GridLayout(Context context) { + this(context, null); + } + + public GridLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public GridLayout(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public GridLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); mDefaultGap = context.getResources().getDimensionPixelOffset(R.dimen.default_gap); - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.GridLayout); + final TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.GridLayout, defStyleAttr, defStyleRes); try { setRowCount(a.getInt(ROW_COUNT, DEFAULT_COUNT)); setColumnCount(a.getInt(COLUMN_COUNT, DEFAULT_COUNT)); @@ -282,21 +305,6 @@ public class GridLayout extends ViewGroup { } } - /** - * {@inheritDoc} - */ - public GridLayout(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - /** - * {@inheritDoc} - */ - public GridLayout(Context context) { - //noinspection NullableProblems - this(context, null); - } - // Implementation /** @@ -308,6 +316,7 @@ public class GridLayout extends ViewGroup { * * @attr ref android.R.styleable#GridLayout_orientation */ + @Orientation public int getOrientation() { return mOrientation; } @@ -348,7 +357,7 @@ public class GridLayout extends ViewGroup { * * @attr ref android.R.styleable#GridLayout_orientation */ - public void setOrientation(int orientation) { + public void setOrientation(@Orientation int orientation) { if (this.mOrientation != orientation) { this.mOrientation = orientation; invalidateStructure(); @@ -479,6 +488,7 @@ public class GridLayout extends ViewGroup { * * @attr ref android.R.styleable#GridLayout_alignmentMode */ + @AlignmentMode public int getAlignmentMode() { return mAlignmentMode; } @@ -498,7 +508,7 @@ public class GridLayout extends ViewGroup { * * @attr ref android.R.styleable#GridLayout_alignmentMode */ - public void setAlignmentMode(int alignmentMode) { + public void setAlignmentMode(@AlignmentMode int alignmentMode) { this.mAlignmentMode = alignmentMode; requestLayout(); } diff --git a/core/java/android/widget/GridView.java b/core/java/android/widget/GridView.java index 15daf83..04b18c1 100644 --- a/core/java/android/widget/GridView.java +++ b/core/java/android/widget/GridView.java @@ -16,26 +16,32 @@ package android.widget; +import android.annotation.IntDef; import android.content.Context; import android.content.Intent; import android.content.res.TypedArray; import android.graphics.Rect; import android.os.Trace; import android.util.AttributeSet; +import android.util.MathUtils; import android.view.Gravity; import android.view.KeyEvent; import android.view.SoundEffectConstants; import android.view.View; import android.view.ViewDebug; import android.view.ViewGroup; +import android.view.ViewRootImpl; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeProvider; import android.view.accessibility.AccessibilityNodeInfo.CollectionInfo; import android.view.accessibility.AccessibilityNodeInfo.CollectionItemInfo; import android.view.animation.GridLayoutAnimationController; -import android.widget.AbsListView.LayoutParams; import android.widget.RemoteViews.RemoteView; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * A view that shows items in two-dimensional scrolling grid. The items in the @@ -53,6 +59,11 @@ import android.widget.RemoteViews.RemoteView; */ @RemoteView public class GridView extends AbsListView { + /** @hide */ + @IntDef({NO_STRETCH, STRETCH_SPACING, STRETCH_COLUMN_WIDTH, STRETCH_SPACING_UNIFORM}) + @Retention(RetentionPolicy.SOURCE) + public @interface StretchMode {} + /** * Disables stretching. * @@ -110,11 +121,15 @@ public class GridView extends AbsListView { this(context, attrs, com.android.internal.R.attr.gridViewStyle); } - public GridView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public GridView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public GridView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.GridView, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.GridView, defStyleAttr, defStyleRes); int hSpacing = a.getDimensionPixelOffset( com.android.internal.R.styleable.GridView_horizontalSpacing, 0); @@ -1014,6 +1029,11 @@ public class GridView extends AbsListView { } @Override + AbsPositionScroller createPositionScroller() { + return new GridViewPositionScroller(); + } + + @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // Sets up mListPadding super.onMeasure(widthMeasureSpec, heightMeasureSpec); @@ -1202,6 +1222,34 @@ public class GridView extends AbsListView { setSelectedPositionInt(mNextSelectedPosition); + AccessibilityNodeInfo accessibilityFocusLayoutRestoreNode = null; + View accessibilityFocusLayoutRestoreView = null; + int accessibilityFocusPosition = INVALID_POSITION; + + // Remember which child, if any, had accessibility focus. This must + // occur before recycling any views, since that will clear + // accessibility focus. + final ViewRootImpl viewRootImpl = getViewRootImpl(); + if (viewRootImpl != null) { + final View focusHost = viewRootImpl.getAccessibilityFocusedHost(); + if (focusHost != null) { + final View focusChild = getAccessibilityFocusedChild(focusHost); + if (focusChild != null) { + if (!dataChanged || focusChild.hasTransientState() + || mAdapterHasStableIds) { + // The views won't be changing, so try to maintain + // focus on the current host and virtual view. + accessibilityFocusLayoutRestoreView = focusHost; + accessibilityFocusLayoutRestoreNode = viewRootImpl + .getAccessibilityFocusedVirtualView(); + } + + // Try to maintain focus at the same position. + accessibilityFocusPosition = getPositionForView(focusChild); + } + } + } + // Pull all children into the RecycleBin. // These views will be reused if possible final int firstPosition = mFirstPosition; @@ -1216,7 +1264,6 @@ public class GridView extends AbsListView { } // Clear out old views - //removeAllViewsInLayout(); detachAllViewsFromParent(); recycleBin.removeSkippedScrap(); @@ -1287,6 +1334,35 @@ public class GridView extends AbsListView { mSelectorRect.setEmpty(); } + // Attempt to restore accessibility focus, if necessary. + if (viewRootImpl != null) { + final View newAccessibilityFocusedView = viewRootImpl.getAccessibilityFocusedHost(); + if (newAccessibilityFocusedView == null) { + if (accessibilityFocusLayoutRestoreView != null + && accessibilityFocusLayoutRestoreView.isAttachedToWindow()) { + final AccessibilityNodeProvider provider = + accessibilityFocusLayoutRestoreView.getAccessibilityNodeProvider(); + if (accessibilityFocusLayoutRestoreNode != null && provider != null) { + final int virtualViewId = AccessibilityNodeInfo.getVirtualDescendantId( + accessibilityFocusLayoutRestoreNode.getSourceNodeId()); + provider.performAction(virtualViewId, + AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); + } else { + accessibilityFocusLayoutRestoreView.requestAccessibilityFocus(); + } + } else if (accessibilityFocusPosition != INVALID_POSITION) { + // Bound the position within the visible children. + final int position = MathUtils.constrain( + accessibilityFocusPosition - mFirstPosition, 0, + getChildCount() - 1); + final View restoreView = getChildAt(position); + if (restoreView != null) { + restoreView.requestAccessibilityFocus(); + } + } + } + } + mLayoutMode = LAYOUT_NORMAL; mDataChanged = false; if (mPositionScrollAfterLayout != null) { @@ -2056,13 +2132,14 @@ public class GridView extends AbsListView { * * @attr ref android.R.styleable#GridView_stretchMode */ - public void setStretchMode(int stretchMode) { + public void setStretchMode(@StretchMode int stretchMode) { if (stretchMode != mStretchMode) { mStretchMode = stretchMode; requestLayoutIfNecessary(); } } + @StretchMode public int getStretchMode() { return mStretchMode; } @@ -2265,7 +2342,9 @@ public class GridView extends AbsListView { final int columnsCount = getNumColumns(); final int rowsCount = getCount() / columnsCount; - final CollectionInfo collectionInfo = CollectionInfo.obtain(columnsCount, rowsCount, false); + final int selectionMode = getSelectionModeForAccessibility(); + final CollectionInfo collectionInfo = CollectionInfo.obtain( + columnsCount, rowsCount, false, selectionMode); info.setCollectionInfo(collectionInfo); } @@ -2292,7 +2371,38 @@ public class GridView extends AbsListView { final LayoutParams lp = (LayoutParams) view.getLayoutParams(); final boolean isHeading = lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER; - final CollectionItemInfo itemInfo = CollectionItemInfo.obtain(column, 1, row, 1, isHeading); + final boolean isSelected = isItemChecked(position); + final CollectionItemInfo itemInfo = CollectionItemInfo.obtain( + column, 1, row, 1, isHeading, isSelected); info.setCollectionItemInfo(itemInfo); } + + /** + * Sub-position scroller that understands the layout of a GridView. + */ + class GridViewPositionScroller extends AbsSubPositionScroller { + @Override + public int getRowForPosition(int position) { + return position / mNumColumns; + } + + @Override + public int getFirstPositionForRow(int row) { + return row * mNumColumns; + } + + @Override + public int getHeightForRow(int row) { + final int firstRowPosition = row * mNumColumns; + final int lastRowPosition = Math.min(getCount(), firstRowPosition + mNumColumns); + int maxHeight = 0; + for (int i = firstRowPosition; i < lastRowPosition; i++) { + final int height = getHeightForPosition(i); + if (height > maxHeight) { + maxHeight = height; + } + } + return maxHeight; + } + } } diff --git a/core/java/android/widget/HorizontalScrollView.java b/core/java/android/widget/HorizontalScrollView.java index dab0962..25d4f42 100644 --- a/core/java/android/widget/HorizontalScrollView.java +++ b/core/java/android/widget/HorizontalScrollView.java @@ -146,12 +146,17 @@ public class HorizontalScrollView extends FrameLayout { this(context, attrs, com.android.internal.R.attr.horizontalScrollViewStyle); } - public HorizontalScrollView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public HorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public HorizontalScrollView( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); initScrollView(); - TypedArray a = context.obtainStyledAttributes(attrs, - android.R.styleable.HorizontalScrollView, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, android.R.styleable.HorizontalScrollView, defStyleAttr, defStyleRes); setFillViewport(a.getBoolean(android.R.styleable.HorizontalScrollView_fillViewport, false)); diff --git a/core/java/android/widget/ImageButton.java b/core/java/android/widget/ImageButton.java index 379354c..3a20628 100644 --- a/core/java/android/widget/ImageButton.java +++ b/core/java/android/widget/ImageButton.java @@ -17,16 +17,11 @@ package android.widget; import android.content.Context; -import android.os.Handler; -import android.os.Message; import android.util.AttributeSet; -import android.view.MotionEvent; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.RemoteViews.RemoteView; -import java.util.Map; - /** * <p> * Displays a button with an image (instead of text) that can be pressed @@ -83,8 +78,12 @@ public class ImageButton extends ImageView { this(context, attrs, com.android.internal.R.attr.imageButtonStyle); } - public ImageButton(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public ImageButton(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ImageButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); setFocusable(true); } diff --git a/core/java/android/widget/ImageView.java b/core/java/android/widget/ImageView.java index f05179b..eedacb5 100644 --- a/core/java/android/widget/ImageView.java +++ b/core/java/android/widget/ImageView.java @@ -24,8 +24,10 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Matrix; +import android.graphics.PixelFormat; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; +import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Xfermode; import android.graphics.drawable.BitmapDrawable; @@ -119,12 +121,17 @@ public class ImageView extends View { this(context, attrs, 0); } - public ImageView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public ImageView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ImageView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + initImageView(); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.ImageView, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.ImageView, defStyleAttr, defStyleRes); Drawable d = a.getDrawable(com.android.internal.R.styleable.ImageView_src); if (d != null) { @@ -357,13 +364,13 @@ public class ImageView extends View { @android.view.RemotableViewMethod public void setImageResource(int resId) { if (mUri != null || mResource != resId) { + final int oldWidth = mDrawableWidth; + final int oldHeight = mDrawableHeight; + updateDrawable(null); mResource = resId; mUri = null; - final int oldWidth = mDrawableWidth; - final int oldHeight = mDrawableHeight; - resolveUri(); if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) { @@ -635,7 +642,7 @@ public class ImageView extends View { if (mResource != 0) { try { - d = rsrc.getDrawable(mResource); + d = mContext.getDrawable(mResource); } catch (Exception e) { Log.w("ImageView", "Unable to find resource: " + mResource, e); // Don't try again. @@ -648,7 +655,7 @@ public class ImageView extends View { // Load drawable through Resources, to get the source density information ContentResolver.OpenResourceIdResult r = mContext.getContentResolver().getResourceId(mUri); - d = r.r.getDrawable(r.id); + d = r.r.getDrawable(r.id, mContext.getTheme()); } catch (Exception e) { Log.w("ImageView", "Unable to open content: " + mUri, e); } @@ -657,7 +664,7 @@ public class ImageView extends View { InputStream stream = null; try { stream = mContext.getContentResolver().openInputStream(mUri); - d = Drawable.createFromStream(stream, null); + d = Drawable.createFromStreamThemed(stream, null, mContext.getTheme()); } catch (Exception e) { Log.w("ImageView", "Unable to open content: " + mUri, e); } finally { @@ -1220,6 +1227,37 @@ public class ImageView extends View { } } + @Override + public boolean isOpaque() { + return super.isOpaque() || mDrawable != null && mXfermode == null + && mDrawable.getOpacity() == PixelFormat.OPAQUE + && mAlpha * mViewAlphaScale >> 8 == 255 + && isFilledByImage(); + } + + private boolean isFilledByImage() { + if (mDrawable == null) { + return false; + } + + final Rect bounds = mDrawable.getBounds(); + final Matrix matrix = mDrawMatrix; + if (matrix == null) { + return bounds.left <= 0 && bounds.top <= 0 && bounds.right >= getWidth() + && bounds.bottom >= getHeight(); + } else if (matrix.rectStaysRect()) { + final RectF boundsSrc = mTempSrc; + final RectF boundsDst = mTempDst; + boundsSrc.set(bounds); + matrix.mapRect(boundsDst, boundsSrc); + return boundsDst.left <= 0 && boundsDst.top <= 0 && boundsDst.right >= getWidth() + && boundsDst.bottom >= getHeight(); + } else { + // If the matrix doesn't map to a rectangle, assume the worst. + return false; + } + } + @RemotableViewMethod @Override public void setVisibility(int visibility) { diff --git a/core/java/android/widget/LegacyTimePickerDelegate.java b/core/java/android/widget/LegacyTimePickerDelegate.java new file mode 100644 index 0000000..1634d5f --- /dev/null +++ b/core/java/android/widget/LegacyTimePickerDelegate.java @@ -0,0 +1,638 @@ +/* + * 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.widget; + +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.TypedArray; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.format.DateFormat; +import android.text.format.DateUtils; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import com.android.internal.R; + +import java.text.DateFormatSymbols; +import java.util.Calendar; +import java.util.Locale; + +import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_AUTO; +import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES; + +/** + * A delegate implementing the basic TimePicker + */ +class LegacyTimePickerDelegate extends TimePicker.AbstractTimePickerDelegate { + + private static final boolean DEFAULT_ENABLED_STATE = true; + + private static final int HOURS_IN_HALF_DAY = 12; + + // state + private boolean mIs24HourView; + + private boolean mIsAm; + + // ui components + private final NumberPicker mHourSpinner; + + private final NumberPicker mMinuteSpinner; + + private final NumberPicker mAmPmSpinner; + + private final EditText mHourSpinnerInput; + + private final EditText mMinuteSpinnerInput; + + private final EditText mAmPmSpinnerInput; + + private final TextView mDivider; + + // Note that the legacy implementation of the TimePicker is + // using a button for toggling between AM/PM while the new + // version uses a NumberPicker spinner. Therefore the code + // accommodates these two cases to be backwards compatible. + private final Button mAmPmButton; + + private final String[] mAmPmStrings; + + private boolean mIsEnabled = DEFAULT_ENABLED_STATE; + + private Calendar mTempCalendar; + + private boolean mHourWithTwoDigit; + private char mHourFormat; + + /** + * A no-op callback used in the constructor to avoid null checks later in + * the code. + */ + private static final TimePicker.OnTimeChangedListener NO_OP_CHANGE_LISTENER = + new TimePicker.OnTimeChangedListener() { + public void onTimeChanged(TimePicker view, int hourOfDay, int minute) { + } + }; + + public LegacyTimePickerDelegate(TimePicker delegator, Context context, AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + super(delegator, context); + + // process style attributes + final TypedArray attributesArray = mContext.obtainStyledAttributes( + attrs, R.styleable.TimePicker, defStyleAttr, defStyleRes); + final int layoutResourceId = attributesArray.getResourceId( + R.styleable.TimePicker_legacyLayout, R.layout.time_picker_legacy); + attributesArray.recycle(); + + final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(layoutResourceId, mDelegator, true); + + // hour + mHourSpinner = (NumberPicker) delegator.findViewById(R.id.hour); + mHourSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() { + public void onValueChange(NumberPicker spinner, int oldVal, int newVal) { + updateInputState(); + if (!is24HourView()) { + if ((oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) || + (oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1)) { + mIsAm = !mIsAm; + updateAmPmControl(); + } + } + onTimeChanged(); + } + }); + mHourSpinnerInput = (EditText) mHourSpinner.findViewById(R.id.numberpicker_input); + mHourSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); + + // divider (only for the new widget style) + mDivider = (TextView) mDelegator.findViewById(R.id.divider); + if (mDivider != null) { + setDividerText(); + } + + // minute + mMinuteSpinner = (NumberPicker) mDelegator.findViewById(R.id.minute); + mMinuteSpinner.setMinValue(0); + mMinuteSpinner.setMaxValue(59); + mMinuteSpinner.setOnLongPressUpdateInterval(100); + mMinuteSpinner.setFormatter(NumberPicker.getTwoDigitFormatter()); + mMinuteSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() { + public void onValueChange(NumberPicker spinner, int oldVal, int newVal) { + updateInputState(); + int minValue = mMinuteSpinner.getMinValue(); + int maxValue = mMinuteSpinner.getMaxValue(); + if (oldVal == maxValue && newVal == minValue) { + int newHour = mHourSpinner.getValue() + 1; + if (!is24HourView() && newHour == HOURS_IN_HALF_DAY) { + mIsAm = !mIsAm; + updateAmPmControl(); + } + mHourSpinner.setValue(newHour); + } else if (oldVal == minValue && newVal == maxValue) { + int newHour = mHourSpinner.getValue() - 1; + if (!is24HourView() && newHour == HOURS_IN_HALF_DAY - 1) { + mIsAm = !mIsAm; + updateAmPmControl(); + } + mHourSpinner.setValue(newHour); + } + onTimeChanged(); + } + }); + mMinuteSpinnerInput = (EditText) mMinuteSpinner.findViewById(R.id.numberpicker_input); + mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); + + /* Get the localized am/pm strings and use them in the spinner */ + mAmPmStrings = new DateFormatSymbols().getAmPmStrings(); + + // am/pm + View amPmView = mDelegator.findViewById(R.id.amPm); + if (amPmView instanceof Button) { + mAmPmSpinner = null; + mAmPmSpinnerInput = null; + mAmPmButton = (Button) amPmView; + mAmPmButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View button) { + button.requestFocus(); + mIsAm = !mIsAm; + updateAmPmControl(); + onTimeChanged(); + } + }); + } else { + mAmPmButton = null; + mAmPmSpinner = (NumberPicker) amPmView; + mAmPmSpinner.setMinValue(0); + mAmPmSpinner.setMaxValue(1); + mAmPmSpinner.setDisplayedValues(mAmPmStrings); + mAmPmSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() { + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + updateInputState(); + picker.requestFocus(); + mIsAm = !mIsAm; + updateAmPmControl(); + onTimeChanged(); + } + }); + mAmPmSpinnerInput = (EditText) mAmPmSpinner.findViewById(R.id.numberpicker_input); + mAmPmSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE); + } + + if (isAmPmAtStart()) { + // Move the am/pm view to the beginning + ViewGroup amPmParent = (ViewGroup) delegator.findViewById(R.id.timePickerLayout); + amPmParent.removeView(amPmView); + amPmParent.addView(amPmView, 0); + // Swap layout margins if needed. They may be not symmetrical (Old Standard Theme + // for example and not for Holo Theme) + ViewGroup.MarginLayoutParams lp = + (ViewGroup.MarginLayoutParams) amPmView.getLayoutParams(); + final int startMargin = lp.getMarginStart(); + final int endMargin = lp.getMarginEnd(); + if (startMargin != endMargin) { + lp.setMarginStart(endMargin); + lp.setMarginEnd(startMargin); + } + } + + getHourFormatData(); + + // update controls to initial state + updateHourControl(); + updateMinuteControl(); + updateAmPmControl(); + + setOnTimeChangedListener(NO_OP_CHANGE_LISTENER); + + // set to current time + setCurrentHour(mTempCalendar.get(Calendar.HOUR_OF_DAY)); + setCurrentMinute(mTempCalendar.get(Calendar.MINUTE)); + + if (!isEnabled()) { + setEnabled(false); + } + + // set the content descriptions + setContentDescriptions(); + + // If not explicitly specified this view is important for accessibility. + if (mDelegator.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + mDelegator.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + } + } + + private void getHourFormatData() { + final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, + (mIs24HourView) ? "Hm" : "hm"); + final int lengthPattern = bestDateTimePattern.length(); + mHourWithTwoDigit = false; + char hourFormat = '\0'; + // Check if the returned pattern is single or double 'H', 'h', 'K', 'k'. We also save + // the hour format that we found. + for (int i = 0; i < lengthPattern; i++) { + final char c = bestDateTimePattern.charAt(i); + if (c == 'H' || c == 'h' || c == 'K' || c == 'k') { + mHourFormat = c; + if (i + 1 < lengthPattern && c == bestDateTimePattern.charAt(i + 1)) { + mHourWithTwoDigit = true; + } + break; + } + } + } + + private boolean isAmPmAtStart() { + final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, + "hm" /* skeleton */); + + return bestDateTimePattern.startsWith("a"); + } + + /** + * The time separator is defined in the Unicode CLDR and cannot be supposed to be ":". + * + * See http://unicode.org/cldr/trac/browser/trunk/common/main + * + * We pass the correct "skeleton" depending on 12 or 24 hours view and then extract the + * separator as the character which is just after the hour marker in the returned pattern. + */ + private void setDividerText() { + final String skeleton = (mIs24HourView) ? "Hm" : "hm"; + final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, + skeleton); + final String separatorText; + int hourIndex = bestDateTimePattern.lastIndexOf('H'); + if (hourIndex == -1) { + hourIndex = bestDateTimePattern.lastIndexOf('h'); + } + if (hourIndex == -1) { + // Default case + separatorText = ":"; + } else { + int minuteIndex = bestDateTimePattern.indexOf('m', hourIndex + 1); + if (minuteIndex == -1) { + separatorText = Character.toString(bestDateTimePattern.charAt(hourIndex + 1)); + } else { + separatorText = bestDateTimePattern.substring(hourIndex + 1, minuteIndex); + } + } + mDivider.setText(separatorText); + } + + @Override + public void setCurrentHour(Integer currentHour) { + setCurrentHour(currentHour, true); + } + + private void setCurrentHour(Integer currentHour, boolean notifyTimeChanged) { + // why was Integer used in the first place? + if (currentHour == null || currentHour == getCurrentHour()) { + return; + } + if (!is24HourView()) { + // convert [0,23] ordinal to wall clock display + if (currentHour >= HOURS_IN_HALF_DAY) { + mIsAm = false; + if (currentHour > HOURS_IN_HALF_DAY) { + currentHour = currentHour - HOURS_IN_HALF_DAY; + } + } else { + mIsAm = true; + if (currentHour == 0) { + currentHour = HOURS_IN_HALF_DAY; + } + } + updateAmPmControl(); + } + mHourSpinner.setValue(currentHour); + if (notifyTimeChanged) { + onTimeChanged(); + } + } + + @Override + public Integer getCurrentHour() { + int currentHour = mHourSpinner.getValue(); + if (is24HourView()) { + return currentHour; + } else if (mIsAm) { + return currentHour % HOURS_IN_HALF_DAY; + } else { + return (currentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY; + } + } + + @Override + public void setCurrentMinute(Integer currentMinute) { + if (currentMinute == getCurrentMinute()) { + return; + } + mMinuteSpinner.setValue(currentMinute); + onTimeChanged(); + } + + @Override + public Integer getCurrentMinute() { + return mMinuteSpinner.getValue(); + } + + @Override + public void setIs24HourView(Boolean is24HourView) { + if (mIs24HourView == is24HourView) { + return; + } + // cache the current hour since spinner range changes and BEFORE changing mIs24HourView!! + int currentHour = getCurrentHour(); + // Order is important here. + mIs24HourView = is24HourView; + getHourFormatData(); + updateHourControl(); + // set value after spinner range is updated + setCurrentHour(currentHour, false); + updateMinuteControl(); + updateAmPmControl(); + } + + @Override + public boolean is24HourView() { + return mIs24HourView; + } + + @Override + public void setOnTimeChangedListener(TimePicker.OnTimeChangedListener onTimeChangedListener) { + mOnTimeChangedListener = onTimeChangedListener; + } + + @Override + public void setEnabled(boolean enabled) { + mMinuteSpinner.setEnabled(enabled); + if (mDivider != null) { + mDivider.setEnabled(enabled); + } + mHourSpinner.setEnabled(enabled); + if (mAmPmSpinner != null) { + mAmPmSpinner.setEnabled(enabled); + } else { + mAmPmButton.setEnabled(enabled); + } + mIsEnabled = enabled; + } + + @Override + public boolean isEnabled() { + return mIsEnabled; + } + + @Override + public void setShowDoneButton(boolean showDoneButton) { + // Nothing to do + } + + @Override + public void setDismissCallback(TimePicker.TimePickerDismissCallback callback) { + // Nothing to do + } + + @Override + public int getBaseline() { + return mHourSpinner.getBaseline(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + setCurrentLocale(newConfig.locale); + } + + @Override + public Parcelable onSaveInstanceState(Parcelable superState) { + return new SavedState(superState, getCurrentHour(), getCurrentMinute()); + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + setCurrentHour(ss.getHour()); + setCurrentMinute(ss.getMinute()); + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + onPopulateAccessibilityEvent(event); + return true; + } + + @Override + public void onPopulateAccessibilityEvent(AccessibilityEvent event) { + int flags = DateUtils.FORMAT_SHOW_TIME; + if (mIs24HourView) { + flags |= DateUtils.FORMAT_24HOUR; + } else { + flags |= DateUtils.FORMAT_12HOUR; + } + mTempCalendar.set(Calendar.HOUR_OF_DAY, getCurrentHour()); + mTempCalendar.set(Calendar.MINUTE, getCurrentMinute()); + String selectedDateUtterance = DateUtils.formatDateTime(mContext, + mTempCalendar.getTimeInMillis(), flags); + event.getText().add(selectedDateUtterance); + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + event.setClassName(TimePicker.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + info.setClassName(TimePicker.class.getName()); + } + + private void updateInputState() { + // Make sure that if the user changes the value and the IME is active + // for one of the inputs if this widget, the IME is closed. If the user + // changed the value via the IME and there is a next input the IME will + // be shown, otherwise the user chose another means of changing the + // value and having the IME up makes no sense. + InputMethodManager inputMethodManager = InputMethodManager.peekInstance(); + if (inputMethodManager != null) { + if (inputMethodManager.isActive(mHourSpinnerInput)) { + mHourSpinnerInput.clearFocus(); + inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0); + } else if (inputMethodManager.isActive(mMinuteSpinnerInput)) { + mMinuteSpinnerInput.clearFocus(); + inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0); + } else if (inputMethodManager.isActive(mAmPmSpinnerInput)) { + mAmPmSpinnerInput.clearFocus(); + inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0); + } + } + } + + private void updateAmPmControl() { + if (is24HourView()) { + if (mAmPmSpinner != null) { + mAmPmSpinner.setVisibility(View.GONE); + } else { + mAmPmButton.setVisibility(View.GONE); + } + } else { + int index = mIsAm ? Calendar.AM : Calendar.PM; + if (mAmPmSpinner != null) { + mAmPmSpinner.setValue(index); + mAmPmSpinner.setVisibility(View.VISIBLE); + } else { + mAmPmButton.setText(mAmPmStrings[index]); + mAmPmButton.setVisibility(View.VISIBLE); + } + } + mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); + } + + /** + * Sets the current locale. + * + * @param locale The current locale. + */ + @Override + public void setCurrentLocale(Locale locale) { + super.setCurrentLocale(locale); + mTempCalendar = Calendar.getInstance(locale); + } + + private void onTimeChanged() { + mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); + if (mOnTimeChangedListener != null) { + mOnTimeChangedListener.onTimeChanged(mDelegator, getCurrentHour(), + getCurrentMinute()); + } + } + + private void updateHourControl() { + if (is24HourView()) { + // 'k' means 1-24 hour + if (mHourFormat == 'k') { + mHourSpinner.setMinValue(1); + mHourSpinner.setMaxValue(24); + } else { + mHourSpinner.setMinValue(0); + mHourSpinner.setMaxValue(23); + } + } else { + // 'K' means 0-11 hour + if (mHourFormat == 'K') { + mHourSpinner.setMinValue(0); + mHourSpinner.setMaxValue(11); + } else { + mHourSpinner.setMinValue(1); + mHourSpinner.setMaxValue(12); + } + } + mHourSpinner.setFormatter(mHourWithTwoDigit ? NumberPicker.getTwoDigitFormatter() : null); + } + + private void updateMinuteControl() { + if (is24HourView()) { + mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE); + } else { + mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); + } + } + + private void setContentDescriptions() { + // Minute + trySetContentDescription(mMinuteSpinner, R.id.increment, + R.string.time_picker_increment_minute_button); + trySetContentDescription(mMinuteSpinner, R.id.decrement, + R.string.time_picker_decrement_minute_button); + // Hour + trySetContentDescription(mHourSpinner, R.id.increment, + R.string.time_picker_increment_hour_button); + trySetContentDescription(mHourSpinner, R.id.decrement, + R.string.time_picker_decrement_hour_button); + // AM/PM + if (mAmPmSpinner != null) { + trySetContentDescription(mAmPmSpinner, R.id.increment, + R.string.time_picker_increment_set_pm_button); + trySetContentDescription(mAmPmSpinner, R.id.decrement, + R.string.time_picker_decrement_set_am_button); + } + } + + private void trySetContentDescription(View root, int viewId, int contDescResId) { + View target = root.findViewById(viewId); + if (target != null) { + target.setContentDescription(mContext.getString(contDescResId)); + } + } + + /** + * Used to save / restore state of time picker + */ + private static class SavedState extends View.BaseSavedState { + + private final int mHour; + + private final int mMinute; + + private SavedState(Parcelable superState, int hour, int minute) { + super(superState); + mHour = hour; + mMinute = minute; + } + + private SavedState(Parcel in) { + super(in); + mHour = in.readInt(); + mMinute = in.readInt(); + } + + public int getHour() { + return mHour; + } + + public int getMinute() { + return mMinute; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(mHour); + dest.writeInt(mMinute); + } + + @SuppressWarnings({"unused", "hiding"}) + public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } +} + diff --git a/core/java/android/widget/LinearLayout.java b/core/java/android/widget/LinearLayout.java index ad60a95..82e624d 100644 --- a/core/java/android/widget/LinearLayout.java +++ b/core/java/android/widget/LinearLayout.java @@ -18,6 +18,7 @@ package android.widget; import com.android.internal.R; +import android.annotation.IntDef; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; @@ -31,6 +32,9 @@ import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.RemoteViews.RemoteView; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * A Layout that arranges its children in a single column or a single row. The direction of @@ -57,9 +61,25 @@ import android.widget.RemoteViews.RemoteView; */ @RemoteView public class LinearLayout extends ViewGroup { + /** @hide */ + @IntDef({HORIZONTAL, VERTICAL}) + @Retention(RetentionPolicy.SOURCE) + public @interface OrientationMode {} + public static final int HORIZONTAL = 0; public static final int VERTICAL = 1; + /** @hide */ + @IntDef(flag = true, + value = { + SHOW_DIVIDER_NONE, + SHOW_DIVIDER_BEGINNING, + SHOW_DIVIDER_MIDDLE, + SHOW_DIVIDER_END + }) + @Retention(RetentionPolicy.SOURCE) + public @interface DividerMode {} + /** * Don't show any dividers. */ @@ -165,18 +185,22 @@ public class LinearLayout extends ViewGroup { private int mDividerPadding; public LinearLayout(Context context) { - super(context); + this(context, null); } public LinearLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } - public LinearLayout(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public LinearLayout(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public LinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.LinearLayout, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.LinearLayout, defStyleAttr, defStyleRes); int index = a.getInt(com.android.internal.R.styleable.LinearLayout_orientation, -1); if (index >= 0) { @@ -214,7 +238,7 @@ public class LinearLayout extends ViewGroup { * {@link #SHOW_DIVIDER_MIDDLE}, or {@link #SHOW_DIVIDER_END}, * or {@link #SHOW_DIVIDER_NONE} to show no dividers. */ - public void setShowDividers(int showDividers) { + public void setShowDividers(@DividerMode int showDividers) { if (showDividers != mShowDividers) { requestLayout(); } @@ -230,6 +254,7 @@ public class LinearLayout extends ViewGroup { * @return A flag set indicating how dividers should be shown around items. * @see #setShowDividers(int) */ + @DividerMode public int getShowDividers() { return mShowDividers; } @@ -642,6 +667,7 @@ public class LinearLayout extends ViewGroup { final int heightMode = MeasureSpec.getMode(heightMeasureSpec); boolean matchWidth = false; + boolean skippedMeasure = false; final int baselineChildIndex = mBaselineAlignedChildIndex; final boolean useLargestChild = mUseLargestChild; @@ -676,6 +702,7 @@ public class LinearLayout extends ViewGroup { // there is any leftover space. final int totalLength = mTotalLength; mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin); + skippedMeasure = true; } else { int oldHeight = Integer.MIN_VALUE; @@ -802,9 +829,10 @@ public class LinearLayout extends ViewGroup { heightSize = heightSizeAndState & MEASURED_SIZE_MASK; // Either expand children with weight to take up available space or - // shrink them if they extend beyond our current bounds + // shrink them if they extend beyond our current bounds. If we skipped + // measurement on any children, we need to measure them now. int delta = heightSize - mTotalLength; - if (delta != 0 && totalWeight > 0.0f) { + if (skippedMeasure || delta != 0 && totalWeight > 0.0f) { float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight; mTotalLength = 0; @@ -970,6 +998,7 @@ public class LinearLayout extends ViewGroup { final int heightMode = MeasureSpec.getMode(heightMeasureSpec); boolean matchHeight = false; + boolean skippedMeasure = false; if (mMaxAscent == null || mMaxDescent == null) { mMaxAscent = new int[VERTICAL_GRAVITY_COUNT]; @@ -1032,6 +1061,8 @@ public class LinearLayout extends ViewGroup { if (baselineAligned) { final int freeSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); child.measure(freeSpec, freeSpec); + } else { + skippedMeasure = true; } } else { int oldWidth = Integer.MIN_VALUE; @@ -1180,9 +1211,10 @@ public class LinearLayout extends ViewGroup { widthSize = widthSizeAndState & MEASURED_SIZE_MASK; // Either expand children with weight to take up available space or - // shrink them if they extend beyond our current bounds + // shrink them if they extend beyond our current bounds. If we skipped + // measurement on any children, we need to measure them now. int delta = widthSize - mTotalLength; - if (delta != 0 && totalWeight > 0.0f) { + if (skippedMeasure || delta != 0 && totalWeight > 0.0f) { float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight; maxAscent[0] = maxAscent[1] = maxAscent[2] = maxAscent[3] = -1; @@ -1673,12 +1705,12 @@ public class LinearLayout extends ViewGroup { /** * Should the layout be a column or a row. - * @param orientation Pass HORIZONTAL or VERTICAL. Default - * value is HORIZONTAL. + * @param orientation Pass {@link #HORIZONTAL} or {@link #VERTICAL}. Default + * value is {@link #HORIZONTAL}. * * @attr ref android.R.styleable#LinearLayout_orientation */ - public void setOrientation(int orientation) { + public void setOrientation(@OrientationMode int orientation) { if (mOrientation != orientation) { mOrientation = orientation; requestLayout(); @@ -1690,6 +1722,7 @@ public class LinearLayout extends ViewGroup { * * @return either {@link #HORIZONTAL} or {@link #VERTICAL} */ + @OrientationMode public int getOrientation() { return mOrientation; } diff --git a/core/java/android/widget/ListPopupWindow.java b/core/java/android/widget/ListPopupWindow.java index 13f3eb6..b47177a 100644 --- a/core/java/android/widget/ListPopupWindow.java +++ b/core/java/android/widget/ListPopupWindow.java @@ -24,6 +24,7 @@ import android.database.DataSetObserver; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Handler; +import android.os.SystemClock; import android.text.TextUtils; import android.util.AttributeSet; import android.util.IntProperty; @@ -33,7 +34,6 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.View.MeasureSpec; -import android.view.View.OnAttachStateChangeListener; import android.view.View.OnTouchListener; import android.view.ViewConfiguration; import android.view.ViewGroup; @@ -843,7 +843,7 @@ public class ListPopupWindow { // to select one of its items if (keyCode != KeyEvent.KEYCODE_SPACE && (mDropDownList.getSelectedItemPosition() >= 0 - || !event.isConfirmKey())) { + || !KeyEvent.isConfirmKey(keyCode))) { int curIndex = mDropDownList.getSelectedItemPosition(); boolean consumed; @@ -931,7 +931,7 @@ public class ListPopupWindow { public boolean onKeyUp(int keyCode, KeyEvent event) { if (isShowing() && mDropDownList.getSelectedItemPosition() >= 0) { boolean consumed = mDropDownList.onKeyUp(keyCode, event); - if (consumed && event.isConfirmKey()) { + if (consumed && KeyEvent.isConfirmKey(keyCode)) { // if the list accepts the key events and the key event was a click, the text view // gets the selected item from the drop down as its content dismiss(); @@ -1226,6 +1226,15 @@ public class ListPopupWindow { forwarding = onTouchForwarded(event) || !onForwardingStopped(); } else { forwarding = onTouchObserved(event) && onForwardingStarted(); + + if (forwarding) { + // Make sure we cancel any ongoing source event stream. + final long now = SystemClock.uptimeMillis(); + final MotionEvent e = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, + 0.0f, 0.0f, 0); + mSrc.onTouchEvent(e); + e.recycle(); + } } mForwarding = forwarding; diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java index 78237c3..5de67c8 100644 --- a/core/java/android/widget/ListView.java +++ b/core/java/android/widget/ListView.java @@ -44,6 +44,7 @@ import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.CollectionInfo; import android.view.accessibility.AccessibilityNodeInfo.CollectionItemInfo; +import android.view.accessibility.AccessibilityNodeProvider; import android.widget.RemoteViews.RemoteView; import java.util.ArrayList; @@ -142,11 +143,15 @@ public class ListView extends AbsListView { this(context, attrs, com.android.internal.R.attr.listViewStyle); } - public ListView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public ListView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.ListView, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.ListView, defStyleAttr, defStyleRes); CharSequence[] entries = a.getTextArray( com.android.internal.R.styleable.ListView_entries); @@ -263,6 +268,7 @@ public class ListView extends AbsListView { info.data = data; info.isSelectable = isSelectable; mHeaderViewInfos.add(info); + mAreAllItemsSelectable &= isSelectable; // Wrap the adapter if it wasn't already wrapped. if (mAdapter != null) { @@ -356,6 +362,7 @@ public class ListView extends AbsListView { info.data = data; info.isSelectable = isSelectable; mFooterViewInfos.add(info); + mAreAllItemsSelectable &= isSelectable; // Wrap the adapter if it wasn't already wrapped. if (mAdapter != null) { @@ -1562,22 +1569,58 @@ public class ListView extends AbsListView { setSelectedPositionInt(mNextSelectedPosition); - // Remember which child, if any, had accessibility focus. - final int accessibilityFocusPosition; - final View accessFocusedChild = getAccessibilityFocusedChild(); - if (accessFocusedChild != null) { - accessibilityFocusPosition = getPositionForView(accessFocusedChild); - accessFocusedChild.setHasTransientState(true); - } else { - accessibilityFocusPosition = INVALID_POSITION; + AccessibilityNodeInfo accessibilityFocusLayoutRestoreNode = null; + View accessibilityFocusLayoutRestoreView = null; + int accessibilityFocusPosition = INVALID_POSITION; + + // Remember which child, if any, had accessibility focus. This must + // occur before recycling any views, since that will clear + // accessibility focus. + final ViewRootImpl viewRootImpl = getViewRootImpl(); + if (viewRootImpl != null) { + final View focusHost = viewRootImpl.getAccessibilityFocusedHost(); + if (focusHost != null) { + final View focusChild = getAccessibilityFocusedChild(focusHost); + if (focusChild != null) { + if (!dataChanged || isDirectChildHeaderOrFooter(focusChild) + || focusChild.hasTransientState() || mAdapterHasStableIds) { + // The views won't be changing, so try to maintain + // focus on the current host and virtual view. + accessibilityFocusLayoutRestoreView = focusHost; + accessibilityFocusLayoutRestoreNode = viewRootImpl + .getAccessibilityFocusedVirtualView(); + } + + // If all else fails, maintain focus at the same + // position. + accessibilityFocusPosition = getPositionForView(focusChild); + } + } } - // Ensure the child containing focus, if any, has transient state. - // If the list data hasn't changed, or if the adapter has stable - // IDs, this will maintain focus. + View focusLayoutRestoreDirectChild = null; + View focusLayoutRestoreView = null; + + // Take focus back to us temporarily to avoid the eventual call to + // clear focus when removing the focused child below from messing + // things up when ViewAncestor assigns focus back to someone else. final View focusedChild = getFocusedChild(); if (focusedChild != null) { - focusedChild.setHasTransientState(true); + // TODO: in some cases focusedChild.getParent() == null + + // We can remember the focused view to restore after re-layout + // if the data hasn't changed, or if the focused position is a + // header or footer. + if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)) { + focusLayoutRestoreDirectChild = focusedChild; + // Remember the specific view that had focus. + focusLayoutRestoreView = findFocus(); + if (focusLayoutRestoreView != null) { + // Tell it we are going to mess with it. + focusLayoutRestoreView.onStartTemporaryDetach(); + } + } + requestFocus(); } // Pull all children into the RecycleBin. @@ -1651,20 +1694,24 @@ public class ListView extends AbsListView { recycleBin.scrapActiveViews(); if (sel != null) { - final boolean shouldPlaceFocus = mItemsCanFocus && hasFocus(); - final boolean maintainedFocus = focusedChild != null && focusedChild.hasFocus(); - if (shouldPlaceFocus && !maintainedFocus && !sel.hasFocus()) { - if (sel.requestFocus()) { - // Successfully placed focus, clear selection. - sel.setSelected(false); - mSelectorRect.setEmpty(); - } else { - // Failed to place focus, clear current (invalid) focus. + // The current selected item should get focus if items are + // focusable. + if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) { + final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild && + focusLayoutRestoreView != null && + focusLayoutRestoreView.requestFocus()) || sel.requestFocus(); + if (!focusWasTaken) { + // Selected item didn't take focus, but we still want to + // make sure something else outside of the selected view + // has focus. final View focused = getFocusedChild(); if (focused != null) { focused.clearFocus(); } positionSelector(INVALID_POSITION, sel); + } else { + sel.setSelected(false); + mSelectorRect.setEmpty(); } } else { positionSelector(INVALID_POSITION, sel); @@ -1682,27 +1729,48 @@ public class ListView extends AbsListView { mSelectedTop = 0; mSelectorRect.setEmpty(); } - } - - if (accessFocusedChild != null) { - accessFocusedChild.setHasTransientState(false); - // If we failed to maintain accessibility focus on the previous - // view, attempt to restore it to the previous position. - if (!accessFocusedChild.isAccessibilityFocused() - && accessibilityFocusPosition != INVALID_POSITION) { - // Bound the position within the visible children. - final int position = MathUtils.constrain( - accessibilityFocusPosition - mFirstPosition, 0, getChildCount() - 1); - final View restoreView = getChildAt(position); - if (restoreView != null) { - restoreView.requestAccessibilityFocus(); + // Even if there is not selected position, we may need to + // restore focus (i.e. something focusable in touch mode). + if (hasFocus() && focusLayoutRestoreView != null) { + focusLayoutRestoreView.requestFocus(); + } + } + + // Attempt to restore accessibility focus, if necessary. + if (viewRootImpl != null) { + final View newAccessibilityFocusedView = viewRootImpl.getAccessibilityFocusedHost(); + if (newAccessibilityFocusedView == null) { + if (accessibilityFocusLayoutRestoreView != null + && accessibilityFocusLayoutRestoreView.isAttachedToWindow()) { + final AccessibilityNodeProvider provider = + accessibilityFocusLayoutRestoreView.getAccessibilityNodeProvider(); + if (accessibilityFocusLayoutRestoreNode != null && provider != null) { + final int virtualViewId = AccessibilityNodeInfo.getVirtualDescendantId( + accessibilityFocusLayoutRestoreNode.getSourceNodeId()); + provider.performAction(virtualViewId, + AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); + } else { + accessibilityFocusLayoutRestoreView.requestAccessibilityFocus(); + } + } else if (accessibilityFocusPosition != INVALID_POSITION) { + // Bound the position within the visible children. + final int position = MathUtils.constrain( + accessibilityFocusPosition - mFirstPosition, 0, + getChildCount() - 1); + final View restoreView = getChildAt(position); + if (restoreView != null) { + restoreView.requestAccessibilityFocus(); + } } } } - if (focusedChild != null) { - focusedChild.setHasTransientState(false); + // Tell focus view we are done mucking with it, if it is still in + // our view hierarchy. + if (focusLayoutRestoreView != null + && focusLayoutRestoreView.getWindowToken() != null) { + focusLayoutRestoreView.onFinishTemporaryDetach(); } mLayoutMode = LAYOUT_NORMAL; @@ -1729,31 +1797,27 @@ public class ListView extends AbsListView { } /** - * @return the direct child that contains accessibility focus, or null if no - * child contains accessibility focus + * @param child a direct child of this list. + * @return Whether child is a header or footer view. */ - private View getAccessibilityFocusedChild() { - final ViewRootImpl viewRootImpl = getViewRootImpl(); - if (viewRootImpl == null) { - return null; - } - - View focusedView = viewRootImpl.getAccessibilityFocusedHost(); - if (focusedView == null) { - return null; - } - - ViewParent viewParent = focusedView.getParent(); - while ((viewParent instanceof View) && (viewParent != this)) { - focusedView = (View) viewParent; - viewParent = viewParent.getParent(); + private boolean isDirectChildHeaderOrFooter(View child) { + final ArrayList<FixedViewInfo> headers = mHeaderViewInfos; + final int numHeaders = headers.size(); + for (int i = 0; i < numHeaders; i++) { + if (child == headers.get(i).view) { + return true; + } } - if (!(viewParent instanceof View)) { - return null; + final ArrayList<FixedViewInfo> footers = mFooterViewInfos; + final int numFooters = footers.size(); + for (int i = 0; i < numFooters; i++) { + if (child == footers.get(i).view) { + return true; + } } - return focusedView; + return false; } /** @@ -1915,45 +1979,6 @@ public class ListView extends AbsListView { } /** - * Sets the selected item and positions the selection y pixels from the top edge - * of the ListView. (If in touch mode, the item will not be selected but it will - * still be positioned appropriately.) - * - * @param position Index (starting at 0) of the data item to be selected. - * @param y The distance from the top edge of the ListView (plus padding) that the - * item will be positioned. - */ - public void setSelectionFromTop(int position, int y) { - if (mAdapter == null) { - return; - } - - if (!isInTouchMode()) { - position = lookForSelectablePosition(position, true); - if (position >= 0) { - setNextSelectedPositionInt(position); - } - } else { - mResurrectToPosition = position; - } - - if (position >= 0) { - mLayoutMode = LAYOUT_SPECIFIC; - mSpecificTop = mListPadding.top + y; - - if (mNeedSync) { - mSyncPosition = position; - mSyncRowId = mAdapter.getItemId(position); - } - - if (mPositionScroller != null) { - mPositionScroller.stop(); - } - requestLayout(); - } - } - - /** * Makes the item at the supplied position selected. * * @param position the position of the item to select @@ -3266,14 +3291,13 @@ public class ListView extends AbsListView { if (drawDividers && (bottom < listBottom) && !(drawOverscrollFooter && isLastItem)) { final int nextIndex = (itemIndex + 1); - // Draw dividers between enabled items, headers and/or - // footers when enabled, and the end of the list. - if (areAllItemsSelectable || ((adapter.isEnabled(itemIndex) - || (headerDividers && isHeader) - || (footerDividers && isFooter)) && (isLastItem - || adapter.isEnabled(nextIndex) - || (headerDividers && (nextIndex < headerCount)) - || (footerDividers && (nextIndex >= footerLimit))))) { + // Draw dividers between enabled items, headers + // and/or footers when enabled and requested, and + // after the last enabled item. + if (adapter.isEnabled(itemIndex) && (headerDividers || !isHeader + && (nextIndex >= headerCount)) && (isLastItem + || adapter.isEnabled(nextIndex) && (footerDividers || !isFooter + && (nextIndex < footerLimit)))) { bounds.top = bottom; bounds.bottom = bottom + dividerHeight; drawDivider(canvas, bounds, i); @@ -3315,14 +3339,13 @@ public class ListView extends AbsListView { if (drawDividers && (top > effectivePaddingTop)) { final boolean isFirstItem = (i == start); final int previousIndex = (itemIndex - 1); - // Draw dividers between enabled items, headers and/or - // footers when enabled, and the end of the list. - if (areAllItemsSelectable || ((adapter.isEnabled(itemIndex) - || (headerDividers && isHeader) - || (footerDividers && isFooter)) && (isFirstItem - || adapter.isEnabled(previousIndex) - || (headerDividers && (previousIndex < headerCount)) - || (footerDividers && (previousIndex >= footerLimit))))) { + // Draw dividers between enabled items, headers + // and/or footers when enabled and requested, and + // before the first enabled item. + if (adapter.isEnabled(itemIndex) && (headerDividers || !isHeader + && (previousIndex >= headerCount)) && (isFirstItem || + adapter.isEnabled(previousIndex) && (footerDividers || !isFooter + && (previousIndex < footerLimit)))) { bounds.top = top - dividerHeight; bounds.bottom = top; // Give the method the child ABOVE the divider, @@ -3771,6 +3794,79 @@ public class ListView extends AbsListView { } @Override + int getHeightForPosition(int position) { + final int height = super.getHeightForPosition(position); + if (shouldAdjustHeightForDivider(position)) { + return height + mDividerHeight; + } + return height; + } + + private boolean shouldAdjustHeightForDivider(int itemIndex) { + final int dividerHeight = mDividerHeight; + final Drawable overscrollHeader = mOverScrollHeader; + final Drawable overscrollFooter = mOverScrollFooter; + final boolean drawOverscrollHeader = overscrollHeader != null; + final boolean drawOverscrollFooter = overscrollFooter != null; + final boolean drawDividers = dividerHeight > 0 && mDivider != null; + + if (drawDividers) { + final boolean fillForMissingDividers = isOpaque() && !super.isOpaque(); + final int itemCount = mItemCount; + final int headerCount = mHeaderViewInfos.size(); + final int footerLimit = (itemCount - mFooterViewInfos.size()); + final boolean isHeader = (itemIndex < headerCount); + final boolean isFooter = (itemIndex >= footerLimit); + final boolean headerDividers = mHeaderDividersEnabled; + final boolean footerDividers = mFooterDividersEnabled; + if ((headerDividers || !isHeader) && (footerDividers || !isFooter)) { + final ListAdapter adapter = mAdapter; + if (!mStackFromBottom) { + final boolean isLastItem = (itemIndex == (itemCount - 1)); + if (!drawOverscrollFooter || !isLastItem) { + final int nextIndex = itemIndex + 1; + // Draw dividers between enabled items, headers + // and/or footers when enabled and requested, and + // after the last enabled item. + if (adapter.isEnabled(itemIndex) && (headerDividers || !isHeader + && (nextIndex >= headerCount)) && (isLastItem + || adapter.isEnabled(nextIndex) && (footerDividers || !isFooter + && (nextIndex < footerLimit)))) { + return true; + } else if (fillForMissingDividers) { + return true; + } + } + } else { + final int start = drawOverscrollHeader ? 1 : 0; + final boolean isFirstItem = (itemIndex == start); + if (!isFirstItem) { + final int previousIndex = (itemIndex - 1); + // Draw dividers between enabled items, headers + // and/or footers when enabled and requested, and + // before the first enabled item. + if (adapter.isEnabled(itemIndex) && (headerDividers || !isHeader + && (previousIndex >= headerCount)) && (isFirstItem || + adapter.isEnabled(previousIndex) && (footerDividers || !isFooter + && (previousIndex < footerLimit)))) { + return true; + } else if (fillForMissingDividers) { + return true; + } + } + } + } + } + + return false; + } + + @Override + AbsPositionScroller createPositionScroller() { + return new ListViewPositionScroller(); + } + + @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); event.setClassName(ListView.class.getName()); @@ -3782,7 +3878,8 @@ public class ListView extends AbsListView { info.setClassName(ListView.class.getName()); final int count = getCount(); - final CollectionInfo collectionInfo = CollectionInfo.obtain(1, count, false); + final int selectionMode = getSelectionModeForAccessibility(); + final CollectionInfo collectionInfo = CollectionInfo.obtain(1, count, false, selectionMode); info.setCollectionInfo(collectionInfo); } @@ -3793,7 +3890,29 @@ public class ListView extends AbsListView { final LayoutParams lp = (LayoutParams) view.getLayoutParams(); final boolean isHeading = lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER; - final CollectionItemInfo itemInfo = CollectionItemInfo.obtain(0, 1, position, 1, isHeading); + final boolean isSelected = isItemChecked(position); + final CollectionItemInfo itemInfo = CollectionItemInfo.obtain( + 0, 1, position, 1, isHeading, isSelected); info.setCollectionItemInfo(itemInfo); } + + /** + * Sub-position scroller that understands the layout of a ListView. + */ + class ListViewPositionScroller extends AbsSubPositionScroller { + @Override + public int getRowForPosition(int position) { + return position; + } + + @Override + public int getFirstPositionForRow(int row) { + return row; + } + + @Override + public int getHeightForRow(int row) { + return getHeightForPosition(row); + } + } } diff --git a/core/java/android/widget/MediaController.java b/core/java/android/widget/MediaController.java index 9c61fd6..546cc5f 100644 --- a/core/java/android/widget/MediaController.java +++ b/core/java/android/widget/MediaController.java @@ -442,7 +442,19 @@ public class MediaController extends FrameLayout { @Override public boolean onTouchEvent(MotionEvent event) { - show(sDefaultTimeout); + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + show(0); // show until hide is called + break; + case MotionEvent.ACTION_UP: + show(sDefaultTimeout); // start timeout + break; + case MotionEvent.ACTION_CANCEL: + hide(); + break; + default: + break; + } return true; } diff --git a/core/java/android/widget/MultiAutoCompleteTextView.java b/core/java/android/widget/MultiAutoCompleteTextView.java index 0b30c84..cbd01b0 100644 --- a/core/java/android/widget/MultiAutoCompleteTextView.java +++ b/core/java/android/widget/MultiAutoCompleteTextView.java @@ -67,8 +67,13 @@ public class MultiAutoCompleteTextView extends AutoCompleteTextView { this(context, attrs, com.android.internal.R.attr.autoCompleteTextViewStyle); } - public MultiAutoCompleteTextView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public MultiAutoCompleteTextView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public MultiAutoCompleteTextView( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); } /* package */ void finishInit() { } diff --git a/core/java/android/widget/NumberPicker.java b/core/java/android/widget/NumberPicker.java index 2c44703..d6fa05a 100644 --- a/core/java/android/widget/NumberPicker.java +++ b/core/java/android/widget/NumberPicker.java @@ -16,6 +16,7 @@ package android.widget; +import android.annotation.IntDef; import android.annotation.Widget; import android.content.Context; import android.content.res.ColorStateList; @@ -53,6 +54,8 @@ import android.view.inputmethod.InputMethodManager; import com.android.internal.R; import libcore.icu.LocaleData; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -493,6 +496,10 @@ public class NumberPicker extends LinearLayout { * Interface to listen for the picker scroll state. */ public interface OnScrollListener { + /** @hide */ + @IntDef({SCROLL_STATE_IDLE, SCROLL_STATE_TOUCH_SCROLL, SCROLL_STATE_FLING}) + @Retention(RetentionPolicy.SOURCE) + public @interface ScrollState {} /** * The view is not scrolling. @@ -518,7 +525,7 @@ public class NumberPicker extends LinearLayout { * {@link #SCROLL_STATE_TOUCH_SCROLL} or * {@link #SCROLL_STATE_IDLE}. */ - public void onScrollStateChange(NumberPicker view, int scrollState); + public void onScrollStateChange(NumberPicker view, @ScrollState int scrollState); } /** @@ -559,14 +566,33 @@ public class NumberPicker extends LinearLayout { * * @param context the application environment. * @param attrs a collection of attributes. - * @param defStyle The default style to apply to this view. + * @param defStyleAttr An attribute in the current theme that contains a + * reference to a style resource that supplies default values for + * the view. Can be 0 to not look for defaults. */ - public NumberPicker(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public NumberPicker(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + /** + * Create a new number picker + * + * @param context the application environment. + * @param attrs a collection of attributes. + * @param defStyleAttr An attribute in the current theme that contains a + * reference to a style resource that supplies default values for + * the view. Can be 0 to not look for defaults. + * @param defStyleRes A resource identifier of a style resource that + * supplies default values for the view, used only if + * defStyleAttr is 0 or can not be found in the theme. Can be 0 + * to not look for defaults. + */ + public NumberPicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); // process style attributes - TypedArray attributesArray = context.obtainStyledAttributes( - attrs, R.styleable.NumberPicker, defStyle, 0); + final TypedArray attributesArray = context.obtainStyledAttributes( + attrs, R.styleable.NumberPicker, defStyleAttr, defStyleRes); final int layoutResId = attributesArray.getResourceId( R.styleable.NumberPicker_internalLayout, DEFAULT_LAYOUT_RESOURCE_ID); diff --git a/core/java/android/widget/OverScroller.java b/core/java/android/widget/OverScroller.java index f218199..7b3dd31 100644 --- a/core/java/android/widget/OverScroller.java +++ b/core/java/android/widget/OverScroller.java @@ -70,7 +70,11 @@ public class OverScroller { * @hide */ public OverScroller(Context context, Interpolator interpolator, boolean flywheel) { - mInterpolator = interpolator; + if (interpolator == null) { + mInterpolator = new Scroller.ViscousFluidInterpolator(); + } else { + mInterpolator = interpolator; + } mFlywheel = flywheel; mScrollerX = new SplineOverScroller(context); mScrollerY = new SplineOverScroller(context); @@ -112,7 +116,11 @@ public class OverScroller { } void setInterpolator(Interpolator interpolator) { - mInterpolator = interpolator; + if (interpolator == null) { + mInterpolator = new Scroller.ViscousFluidInterpolator(); + } else { + mInterpolator = interpolator; + } } /** @@ -302,14 +310,7 @@ public class OverScroller { final int duration = mScrollerX.mDuration; if (elapsedTime < duration) { - float q = (float) (elapsedTime) / duration; - - if (mInterpolator == null) { - q = Scroller.viscousFluid(q); - } else { - q = mInterpolator.getInterpolation(q); - } - + final float q = mInterpolator.getInterpolation(elapsedTime / (float) duration); mScrollerX.updateScroll(q); mScrollerY.updateScroll(q); } else { diff --git a/core/java/android/widget/PopupWindow.java b/core/java/android/widget/PopupWindow.java index be20d2d..6e71a5c 100644 --- a/core/java/android/widget/PopupWindow.java +++ b/core/java/android/widget/PopupWindow.java @@ -170,8 +170,8 @@ public class PopupWindow { * * <p>The popup does provide a background.</p> */ - public PopupWindow(Context context, AttributeSet attrs, int defStyle) { - this(context, attrs, defStyle, 0); + public PopupWindow(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); } /** @@ -183,8 +183,7 @@ public class PopupWindow { mContext = context; mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); - TypedArray a = - context.obtainStyledAttributes( + final TypedArray a = context.obtainStyledAttributes( attrs, com.android.internal.R.styleable.PopupWindow, defStyleAttr, defStyleRes); mBackground = a.getDrawable(R.styleable.PopupWindow_popupBackground); diff --git a/core/java/android/widget/ProgressBar.java b/core/java/android/widget/ProgressBar.java index 6a369a6..f7e81b8 100644 --- a/core/java/android/widget/ProgressBar.java +++ b/core/java/android/widget/ProgressBar.java @@ -242,29 +242,26 @@ public class ProgressBar extends View { this(context, attrs, com.android.internal.R.attr.progressBarStyle); } - public ProgressBar(Context context, AttributeSet attrs, int defStyle) { - this(context, attrs, defStyle, 0); + public ProgressBar(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); } - /** - * @hide - */ - public ProgressBar(Context context, AttributeSet attrs, int defStyle, int styleRes) { - super(context, attrs, defStyle); + public ProgressBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + mUiThreadId = Thread.currentThread().getId(); initProgressBar(); - TypedArray a = - context.obtainStyledAttributes(attrs, R.styleable.ProgressBar, defStyle, styleRes); + final TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.ProgressBar, defStyleAttr, defStyleRes); mNoInvalidate = true; Drawable drawable = a.getDrawable(R.styleable.ProgressBar_progressDrawable); if (drawable != null) { - drawable = tileify(drawable, false); // Calling this method can set mMaxHeight, make sure the corresponding // XML attribute for mMaxHeight is read after calling this method - setProgressDrawable(drawable); + setProgressDrawableTiled(drawable); } @@ -293,8 +290,7 @@ public class ProgressBar extends View { drawable = a.getDrawable(R.styleable.ProgressBar_indeterminateDrawable); if (drawable != null) { - drawable = tileifyIndeterminate(drawable); - setIndeterminateDrawable(drawable); + setIndeterminateDrawableTiled(drawable); } mOnlyIndeterminate = a.getBoolean( @@ -350,19 +346,24 @@ public class ProgressBar extends View { return out; } else if (drawable instanceof BitmapDrawable) { - final Bitmap tileBitmap = ((BitmapDrawable) drawable).getBitmap(); + final BitmapDrawable bitmap = (BitmapDrawable) drawable; + final Bitmap tileBitmap = bitmap.getBitmap(); if (mSampleTile == null) { mSampleTile = tileBitmap; } - - final ShapeDrawable shapeDrawable = new ShapeDrawable(getDrawableShape()); + final ShapeDrawable shapeDrawable = new ShapeDrawable(getDrawableShape()); final BitmapShader bitmapShader = new BitmapShader(tileBitmap, Shader.TileMode.REPEAT, Shader.TileMode.CLAMP); shapeDrawable.getPaint().setShader(bitmapShader); - return (clip) ? new ClipDrawable(shapeDrawable, Gravity.LEFT, - ClipDrawable.HORIZONTAL) : shapeDrawable; + // Ensure the color filter and tint are propagated. + shapeDrawable.setTint(bitmap.getTint()); + shapeDrawable.setTintMode(bitmap.getTintMode()); + shapeDrawable.setColorFilter(bitmap.getColorFilter()); + + return clip ? new ClipDrawable( + shapeDrawable, Gravity.LEFT, ClipDrawable.HORIZONTAL) : shapeDrawable; } return drawable; @@ -472,11 +473,9 @@ public class ProgressBar extends View { } /** - * <p>Define the drawable used to draw the progress bar in - * indeterminate mode.</p> + * Define the drawable used to draw the progress bar in indeterminate mode. * * @param d the new drawable - * * @see #getIndeterminateDrawable() * @see #setIndeterminate(boolean) */ @@ -493,6 +492,25 @@ public class ProgressBar extends View { postInvalidate(); } } + + /** + * Define the tileable drawable used to draw the progress bar in + * indeterminate mode. + * <p> + * If the drawable is a BitmapDrawable or contains BitmapDrawables, a + * tiled copy will be generated for display as a progress bar. + * + * @param d the new drawable + * @see #getIndeterminateDrawable() + * @see #setIndeterminate(boolean) + */ + public void setIndeterminateDrawableTiled(Drawable d) { + if (d != null) { + d = tileifyIndeterminate(d); + } + + setIndeterminateDrawable(d); + } /** * <p>Get the drawable used to draw the progress bar in @@ -508,11 +526,9 @@ public class ProgressBar extends View { } /** - * <p>Define the drawable used to draw the progress bar in - * progress mode.</p> + * Define the drawable used to draw the progress bar in progress mode. * * @param d the new drawable - * * @see #getProgressDrawable() * @see #setIndeterminate(boolean) */ @@ -551,6 +567,25 @@ public class ProgressBar extends View { doRefreshProgress(R.id.secondaryProgress, mSecondaryProgress, false, false); } } + + /** + * Define the tileable drawable used to draw the progress bar in + * progress mode. + * <p> + * If the drawable is a BitmapDrawable or contains BitmapDrawables, a + * tiled copy will be generated for display as a progress bar. + * + * @param d the new drawable + * @see #getProgressDrawable() + * @see #setIndeterminate(boolean) + */ + public void setProgressDrawableTiled(Drawable d) { + if (d != null) { + d = tileify(d, false); + } + + setProgressDrawable(d); + } /** * @return The drawable currently used to draw the progress bar diff --git a/core/java/android/widget/QuickContactBadge.java b/core/java/android/widget/QuickContactBadge.java index fd2f754..74b41c9 100644 --- a/core/java/android/widget/QuickContactBadge.java +++ b/core/java/android/widget/QuickContactBadge.java @@ -84,8 +84,13 @@ public class QuickContactBadge extends ImageView implements OnClickListener { this(context, attrs, 0); } - public QuickContactBadge(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public QuickContactBadge(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public QuickContactBadge( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); TypedArray styledAttributes = mContext.obtainStyledAttributes(R.styleable.Theme); mOverlay = styledAttributes.getDrawable( @@ -150,7 +155,7 @@ public class QuickContactBadge extends ImageView implements OnClickListener { */ public void setImageToDefault() { if (mDefaultAvatar == null) { - mDefaultAvatar = getResources().getDrawable(R.drawable.ic_contact_picture); + mDefaultAvatar = mContext.getDrawable(R.drawable.ic_contact_picture); } setImageDrawable(mDefaultAvatar); } diff --git a/core/java/android/widget/RadialTimePickerView.java b/core/java/android/widget/RadialTimePickerView.java new file mode 100644 index 0000000..1c9ab61 --- /dev/null +++ b/core/java/android/widget/RadialTimePickerView.java @@ -0,0 +1,1396 @@ +/* + * 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.widget; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.Keyframe; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Typeface; +import android.graphics.RectF; +import android.os.Bundle; +import android.text.format.DateUtils; +import android.text.format.Time; +import android.util.AttributeSet; +import android.util.Log; +import android.view.HapticFeedbackConstants; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import com.android.internal.R; + +import java.text.DateFormatSymbols; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Locale; + +/** + * View to show a clock circle picker (with one or two picking circles) + * + * @hide + */ +public class RadialTimePickerView extends View implements View.OnTouchListener { + private static final String TAG = "ClockView"; + + private static final boolean DEBUG = false; + + private static final int DEBUG_COLOR = 0x20FF0000; + private static final int DEBUG_TEXT_COLOR = 0x60FF0000; + private static final int DEBUG_STROKE_WIDTH = 2; + + private static final int HOURS = 0; + private static final int MINUTES = 1; + private static final int HOURS_INNER = 2; + private static final int AMPM = 3; + + private static final int SELECTOR_CIRCLE = 0; + private static final int SELECTOR_DOT = 1; + private static final int SELECTOR_LINE = 2; + + private static final int AM = 0; + private static final int PM = 1; + + // Opaque alpha level + private static final int ALPHA_OPAQUE = 255; + + // Transparent alpha level + private static final int ALPHA_TRANSPARENT = 0; + + // Alpha level of color for selector. + private static final int ALPHA_SELECTOR = 51; + + // Alpha level of color for selected circle. + private static final int ALPHA_AMPM_SELECTED = ALPHA_SELECTOR; + + // Alpha level of color for pressed circle. + private static final int ALPHA_AMPM_PRESSED = 175; + + private static final float COSINE_30_DEGREES = ((float) Math.sqrt(3)) * 0.5f; + private static final float SINE_30_DEGREES = 0.5f; + + private static final int DEGREES_FOR_ONE_HOUR = 30; + private static final int DEGREES_FOR_ONE_MINUTE = 6; + + private static final int[] HOURS_NUMBERS = {12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; + private static final int[] HOURS_NUMBERS_24 = {0, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}; + private static final int[] MINUTES_NUMBERS = {0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55}; + + private static final int CENTER_RADIUS = 2; + + private static int[] sSnapPrefer30sMap = new int[361]; + + private final String[] mHours12Texts = new String[12]; + private final String[] mOuterHours24Texts = new String[12]; + private final String[] mInnerHours24Texts = new String[12]; + private final String[] mMinutesTexts = new String[12]; + + private final String[] mAmPmText = new String[2]; + + private final Paint[] mPaint = new Paint[2]; + private final Paint mPaintCenter = new Paint(); + private final Paint[][] mPaintSelector = new Paint[2][3]; + private final Paint mPaintAmPmText = new Paint(); + private final Paint[] mPaintAmPmCircle = new Paint[2]; + + private final Paint mPaintBackground = new Paint(); + private final Paint mPaintDisabled = new Paint(); + private final Paint mPaintDebug = new Paint(); + + private Typeface mTypeface; + + private boolean mIs24HourMode; + private boolean mShowHours; + private boolean mIsOnInnerCircle; + + private int mXCenter; + private int mYCenter; + + private float[] mCircleRadius = new float[3]; + + private int mMinHypotenuseForInnerNumber; + private int mMaxHypotenuseForOuterNumber; + private int mHalfwayHypotenusePoint; + + private float[] mTextSize = new float[2]; + private float mInnerTextSize; + + private float[][] mTextGridHeights = new float[2][7]; + private float[][] mTextGridWidths = new float[2][7]; + + private float[] mInnerTextGridHeights = new float[7]; + private float[] mInnerTextGridWidths = new float[7]; + + private String[] mOuterTextHours; + private String[] mInnerTextHours; + private String[] mOuterTextMinutes; + + private float[] mCircleRadiusMultiplier = new float[2]; + private float[] mNumbersRadiusMultiplier = new float[3]; + + private float[] mTextSizeMultiplier = new float[3]; + + private float[] mAnimationRadiusMultiplier = new float[3]; + + private float mTransitionMidRadiusMultiplier; + private float mTransitionEndRadiusMultiplier; + + private AnimatorSet mTransition; + private InvalidateUpdateListener mInvalidateUpdateListener = new InvalidateUpdateListener(); + + private int[] mLineLength = new int[3]; + private int[] mSelectionRadius = new int[3]; + private float mSelectionRadiusMultiplier; + private int[] mSelectionDegrees = new int[3]; + + private int mAmPmCircleRadius; + private float mAmPmYCenter; + + private float mAmPmCircleRadiusMultiplier; + private int mAmPmTextColor; + + private float mLeftIndicatorXCenter; + private float mRightIndicatorXCenter; + + private int mAmPmUnselectedColor; + private int mAmPmSelectedColor; + + private int mAmOrPm; + private int mAmOrPmPressed; + + private RectF mRectF = new RectF(); + private boolean mInputEnabled = true; + private OnValueSelectedListener mListener; + + private final ArrayList<Animator> mHoursToMinutesAnims = new ArrayList<Animator>(); + private final ArrayList<Animator> mMinuteToHoursAnims = new ArrayList<Animator>(); + + public interface OnValueSelectedListener { + void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance); + } + + static { + // Prepare mapping to snap touchable degrees to selectable degrees. + preparePrefer30sMap(); + } + + /** + * Split up the 360 degrees of the circle among the 60 selectable values. Assigns a larger + * selectable area to each of the 12 visible values, such that the ratio of space apportioned + * to a visible value : space apportioned to a non-visible value will be 14 : 4. + * E.g. the output of 30 degrees should have a higher range of input associated with it than + * the output of 24 degrees, because 30 degrees corresponds to a visible number on the clock + * circle (5 on the minutes, 1 or 13 on the hours). + */ + private static void preparePrefer30sMap() { + // We'll split up the visible output and the non-visible output such that each visible + // output will correspond to a range of 14 associated input degrees, and each non-visible + // output will correspond to a range of 4 associate input degrees, so visible numbers + // are more than 3 times easier to get than non-visible numbers: + // {354-359,0-7}:0, {8-11}:6, {12-15}:12, {16-19}:18, {20-23}:24, {24-37}:30, etc. + // + // If an output of 30 degrees should correspond to a range of 14 associated degrees, then + // we'll need any input between 24 - 37 to snap to 30. Working out from there, 20-23 should + // snap to 24, while 38-41 should snap to 36. This is somewhat counter-intuitive, that you + // can be touching 36 degrees but have the selection snapped to 30 degrees; however, this + // inconsistency isn't noticeable at such fine-grained degrees, and it affords us the + // ability to aggressively prefer the visible values by a factor of more than 3:1, which + // greatly contributes to the selectability of these values. + + // The first output is 0, and each following output will increment by 6 {0, 6, 12, ...}. + int snappedOutputDegrees = 0; + // Count of how many inputs we've designated to the specified output. + int count = 1; + // How many input we expect for a specified output. This will be 14 for output divisible + // by 30, and 4 for the remaining output. We'll special case the outputs of 0 and 360, so + // the caller can decide which they need. + int expectedCount = 8; + // Iterate through the input. + for (int degrees = 0; degrees < 361; degrees++) { + // Save the input-output mapping. + sSnapPrefer30sMap[degrees] = snappedOutputDegrees; + // If this is the last input for the specified output, calculate the next output and + // the next expected count. + if (count == expectedCount) { + snappedOutputDegrees += 6; + if (snappedOutputDegrees == 360) { + expectedCount = 7; + } else if (snappedOutputDegrees % 30 == 0) { + expectedCount = 14; + } else { + expectedCount = 4; + } + count = 1; + } else { + count++; + } + } + } + + /** + * Returns mapping of any input degrees (0 to 360) to one of 60 selectable output degrees, + * where the degrees corresponding to visible numbers (i.e. those divisible by 30) will be + * weighted heavier than the degrees corresponding to non-visible numbers. + * See {@link #preparePrefer30sMap()} documentation for the rationale and generation of the + * mapping. + */ + private static int snapPrefer30s(int degrees) { + if (sSnapPrefer30sMap == null) { + return -1; + } + return sSnapPrefer30sMap[degrees]; + } + + /** + * Returns mapping of any input degrees (0 to 360) to one of 12 visible output degrees (all + * multiples of 30), where the input will be "snapped" to the closest visible degrees. + * @param degrees The input degrees + * @param forceHigherOrLower The output may be forced to either the higher or lower step, or may + * be allowed to snap to whichever is closer. Use 1 to force strictly higher, -1 to force + * strictly lower, and 0 to snap to the closer one. + * @return output degrees, will be a multiple of 30 + */ + private static int snapOnly30s(int degrees, int forceHigherOrLower) { + final int stepSize = DEGREES_FOR_ONE_HOUR; + int floor = (degrees / stepSize) * stepSize; + final int ceiling = floor + stepSize; + if (forceHigherOrLower == 1) { + degrees = ceiling; + } else if (forceHigherOrLower == -1) { + if (degrees == floor) { + floor -= stepSize; + } + degrees = floor; + } else { + if ((degrees - floor) < (ceiling - degrees)) { + degrees = floor; + } else { + degrees = ceiling; + } + } + return degrees; + } + + public RadialTimePickerView(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.timePickerStyle); + } + + public RadialTimePickerView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs); + + // process style attributes + final TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.TimePicker, + defStyle, 0); + + final Resources res = getResources(); + + mAmPmUnselectedColor = a.getColor(R.styleable.TimePicker_amPmUnselectedBackgroundColor, + res.getColor( + R.color.timepicker_default_ampm_unselected_background_color_holo_light)); + + mAmPmSelectedColor = a.getColor(R.styleable.TimePicker_amPmSelectedBackgroundColor, + res.getColor(R.color.timepicker_default_ampm_selected_background_color_holo_light)); + + mAmPmTextColor = a.getColor(R.styleable.TimePicker_amPmTextColor, + res.getColor(R.color.timepicker_default_text_color_holo_light)); + + final int numbersTextColor = a.getColor(R.styleable.TimePicker_numbersTextColor, + res.getColor(R.color.timepicker_default_text_color_holo_light)); + + mTypeface = Typeface.create("sans-serif", Typeface.NORMAL); + + mPaint[HOURS] = new Paint(); + mPaint[HOURS].setColor(numbersTextColor); + mPaint[HOURS].setAntiAlias(true); + mPaint[HOURS].setTextAlign(Paint.Align.CENTER); + + mPaint[MINUTES] = new Paint(); + mPaint[MINUTES].setColor(numbersTextColor); + mPaint[MINUTES].setAntiAlias(true); + mPaint[MINUTES].setTextAlign(Paint.Align.CENTER); + + mPaintCenter.setColor(numbersTextColor); + mPaintCenter.setAntiAlias(true); + mPaintCenter.setTextAlign(Paint.Align.CENTER); + + mPaintSelector[HOURS][SELECTOR_CIRCLE] = new Paint(); + mPaintSelector[HOURS][SELECTOR_CIRCLE].setColor( + a.getColor(R.styleable.TimePicker_numbersSelectorColor, R.color.holo_blue_light)); + mPaintSelector[HOURS][SELECTOR_CIRCLE].setAntiAlias(true); + + mPaintSelector[HOURS][SELECTOR_DOT] = new Paint(); + mPaintSelector[HOURS][SELECTOR_DOT].setColor( + a.getColor(R.styleable.TimePicker_numbersSelectorColor, R.color.holo_blue_light)); + mPaintSelector[HOURS][SELECTOR_DOT].setAntiAlias(true); + + mPaintSelector[HOURS][SELECTOR_LINE] = new Paint(); + mPaintSelector[HOURS][SELECTOR_LINE].setColor( + a.getColor(R.styleable.TimePicker_numbersSelectorColor, R.color.holo_blue_light)); + mPaintSelector[HOURS][SELECTOR_LINE].setAntiAlias(true); + mPaintSelector[HOURS][SELECTOR_LINE].setStrokeWidth(2); + + mPaintSelector[MINUTES][SELECTOR_CIRCLE] = new Paint(); + mPaintSelector[MINUTES][SELECTOR_CIRCLE].setColor( + a.getColor(R.styleable.TimePicker_numbersSelectorColor, R.color.holo_blue_light)); + mPaintSelector[MINUTES][SELECTOR_CIRCLE].setAntiAlias(true); + + mPaintSelector[MINUTES][SELECTOR_DOT] = new Paint(); + mPaintSelector[MINUTES][SELECTOR_DOT].setColor( + a.getColor(R.styleable.TimePicker_numbersSelectorColor, R.color.holo_blue_light)); + mPaintSelector[MINUTES][SELECTOR_DOT].setAntiAlias(true); + + mPaintSelector[MINUTES][SELECTOR_LINE] = new Paint(); + mPaintSelector[MINUTES][SELECTOR_LINE].setColor( + a.getColor(R.styleable.TimePicker_numbersSelectorColor, R.color.holo_blue_light)); + mPaintSelector[MINUTES][SELECTOR_LINE].setAntiAlias(true); + mPaintSelector[MINUTES][SELECTOR_LINE].setStrokeWidth(2); + + mPaintAmPmText.setColor(mAmPmTextColor); + mPaintAmPmText.setTypeface(mTypeface); + mPaintAmPmText.setAntiAlias(true); + mPaintAmPmText.setTextAlign(Paint.Align.CENTER); + + mPaintAmPmCircle[AM] = new Paint(); + mPaintAmPmCircle[AM].setAntiAlias(true); + mPaintAmPmCircle[PM] = new Paint(); + mPaintAmPmCircle[PM].setAntiAlias(true); + + mPaintBackground.setColor( + a.getColor(R.styleable.TimePicker_numbersBackgroundColor, Color.WHITE)); + mPaintBackground.setAntiAlias(true); + + final int disabledColor = a.getColor(R.styleable.TimePicker_disabledColor, + res.getColor(R.color.timepicker_default_disabled_color_holo_light)); + mPaintDisabled.setColor(disabledColor); + mPaintDisabled.setAntiAlias(true); + + if (DEBUG) { + mPaintDebug.setColor(DEBUG_COLOR); + mPaintDebug.setAntiAlias(true); + mPaintDebug.setStrokeWidth(DEBUG_STROKE_WIDTH); + mPaintDebug.setStyle(Paint.Style.STROKE); + mPaintDebug.setTextAlign(Paint.Align.CENTER); + } + + mShowHours = true; + mIs24HourMode = false; + mAmOrPm = AM; + mAmOrPmPressed = -1; + + initHoursAndMinutesText(); + initData(); + + mTransitionMidRadiusMultiplier = Float.parseFloat( + res.getString(R.string.timepicker_transition_mid_radius_multiplier)); + mTransitionEndRadiusMultiplier = Float.parseFloat( + res.getString(R.string.timepicker_transition_end_radius_multiplier)); + + mTextGridHeights[HOURS] = new float[7]; + mTextGridHeights[MINUTES] = new float[7]; + + mSelectionRadiusMultiplier = Float.parseFloat( + res.getString(R.string.timepicker_selection_radius_multiplier)); + + setOnTouchListener(this); + + // Initial values + final Calendar calendar = Calendar.getInstance(Locale.getDefault()); + final int currentHour = calendar.get(Calendar.HOUR_OF_DAY); + final int currentMinute = calendar.get(Calendar.MINUTE); + + setCurrentHour(currentHour); + setCurrentMinute(currentMinute); + + setHapticFeedbackEnabled(true); + } + + /** + * Measure the view to end up as a square, based on the minimum of the height and width. + */ + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int measuredWidth = MeasureSpec.getSize(widthMeasureSpec); + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int measuredHeight = MeasureSpec.getSize(heightMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int minDimension = Math.min(measuredWidth, measuredHeight); + + super.onMeasure(MeasureSpec.makeMeasureSpec(minDimension, widthMode), + MeasureSpec.makeMeasureSpec(minDimension, heightMode)); + } + + public void initialize(int hour, int minute, boolean is24HourMode) { + mIs24HourMode = is24HourMode; + setCurrentHour(hour); + setCurrentMinute(minute); + } + + public void setCurrentItemShowing(int item, boolean animate) { + switch (item){ + case HOURS: + showHours(animate); + break; + case MINUTES: + showMinutes(animate); + break; + default: + Log.e(TAG, "ClockView does not support showing item " + item); + } + } + + public int getCurrentItemShowing() { + return mShowHours ? HOURS : MINUTES; + } + + public void setOnValueSelectedListener(OnValueSelectedListener listener) { + mListener = listener; + } + + public void setCurrentHour(int hour) { + final int degrees = (hour % 12) * DEGREES_FOR_ONE_HOUR; + mSelectionDegrees[HOURS] = degrees; + mSelectionDegrees[HOURS_INNER] = degrees; + mAmOrPm = ((hour % 24) < 12) ? AM : PM; + if (mIs24HourMode) { + mIsOnInnerCircle = (mAmOrPm == AM); + } else { + mIsOnInnerCircle = false; + } + initData(); + updateLayoutData(); + invalidate(); + } + + // Return hours in 0-23 range + public int getCurrentHour() { + int hours = + mSelectionDegrees[mIsOnInnerCircle ? HOURS_INNER : HOURS] / DEGREES_FOR_ONE_HOUR; + if (mIs24HourMode) { + if (mIsOnInnerCircle) { + hours = hours % 12; + } else { + if (hours != 0) { + hours += 12; + } + } + } else { + hours = hours % 12; + if (hours == 0) { + if (mAmOrPm == PM) { + hours = 12; + } + } else { + if (mAmOrPm == PM) { + hours += 12; + } + } + } + return hours; + } + + public void setCurrentMinute(int minute) { + mSelectionDegrees[MINUTES] = (minute % 60) * DEGREES_FOR_ONE_MINUTE; + invalidate(); + } + + // Returns minutes in 0-59 range + public int getCurrentMinute() { + return (mSelectionDegrees[MINUTES] / DEGREES_FOR_ONE_MINUTE); + } + + public void setAmOrPm(int val) { + mAmOrPm = (val % 2); + invalidate(); + } + + public int getAmOrPm() { + return mAmOrPm; + } + + public void swapAmPm() { + mAmOrPm = (mAmOrPm == AM) ? PM : AM; + invalidate(); + } + + public void showHours(boolean animate) { + if (mShowHours) return; + mShowHours = true; + if (animate) { + startMinutesToHoursAnimation(); + } + initData(); + updateLayoutData(); + invalidate(); + } + + public void showMinutes(boolean animate) { + if (!mShowHours) return; + mShowHours = false; + if (animate) { + startHoursToMinutesAnimation(); + } + initData(); + updateLayoutData(); + invalidate(); + } + + private void initHoursAndMinutesText() { + // Initialize the hours and minutes numbers. + for (int i = 0; i < 12; i++) { + mHours12Texts[i] = String.format("%d", HOURS_NUMBERS[i]); + mOuterHours24Texts[i] = String.format("%02d", HOURS_NUMBERS_24[i]); + mInnerHours24Texts[i] = String.format("%d", HOURS_NUMBERS[i]); + mMinutesTexts[i] = String.format("%02d", MINUTES_NUMBERS[i]); + } + + String[] amPmTexts = new DateFormatSymbols().getAmPmStrings(); + mAmPmText[AM] = amPmTexts[0]; + mAmPmText[PM] = amPmTexts[1]; + } + + private void initData() { + if (mIs24HourMode) { + mOuterTextHours = mOuterHours24Texts; + mInnerTextHours = mInnerHours24Texts; + } else { + mOuterTextHours = mHours12Texts; + mInnerTextHours = null; + } + + mOuterTextMinutes = mMinutesTexts; + + final Resources res = getResources(); + + if (mShowHours) { + if (mIs24HourMode) { + mCircleRadiusMultiplier[HOURS] = Float.parseFloat( + res.getString(R.string.timepicker_circle_radius_multiplier_24HourMode)); + mNumbersRadiusMultiplier[HOURS] = Float.parseFloat( + res.getString(R.string.timepicker_numbers_radius_multiplier_outer)); + mTextSizeMultiplier[HOURS] = Float.parseFloat( + res.getString(R.string.timepicker_text_size_multiplier_outer)); + + mNumbersRadiusMultiplier[HOURS_INNER] = Float.parseFloat( + res.getString(R.string.timepicker_numbers_radius_multiplier_inner)); + mTextSizeMultiplier[HOURS_INNER] = Float.parseFloat( + res.getString(R.string.timepicker_text_size_multiplier_inner)); + } else { + mCircleRadiusMultiplier[HOURS] = Float.parseFloat( + res.getString(R.string.timepicker_circle_radius_multiplier)); + mNumbersRadiusMultiplier[HOURS] = Float.parseFloat( + res.getString(R.string.timepicker_numbers_radius_multiplier_normal)); + mTextSizeMultiplier[HOURS] = Float.parseFloat( + res.getString(R.string.timepicker_text_size_multiplier_normal)); + } + } else { + mCircleRadiusMultiplier[MINUTES] = Float.parseFloat( + res.getString(R.string.timepicker_circle_radius_multiplier)); + mNumbersRadiusMultiplier[MINUTES] = Float.parseFloat( + res.getString(R.string.timepicker_numbers_radius_multiplier_normal)); + mTextSizeMultiplier[MINUTES] = Float.parseFloat( + res.getString(R.string.timepicker_text_size_multiplier_normal)); + } + + mAnimationRadiusMultiplier[HOURS] = 1; + mAnimationRadiusMultiplier[HOURS_INNER] = 1; + mAnimationRadiusMultiplier[MINUTES] = 1; + + mAmPmCircleRadiusMultiplier = Float.parseFloat( + res.getString(R.string.timepicker_ampm_circle_radius_multiplier)); + + mPaint[HOURS].setAlpha(mShowHours ? ALPHA_OPAQUE : ALPHA_TRANSPARENT); + mPaint[MINUTES].setAlpha(mShowHours ? ALPHA_TRANSPARENT : ALPHA_OPAQUE); + + mPaintSelector[HOURS][SELECTOR_CIRCLE].setAlpha( + mShowHours ?ALPHA_SELECTOR : ALPHA_TRANSPARENT); + mPaintSelector[HOURS][SELECTOR_DOT].setAlpha( + mShowHours ? ALPHA_OPAQUE : ALPHA_TRANSPARENT); + mPaintSelector[HOURS][SELECTOR_LINE].setAlpha( + mShowHours ? ALPHA_SELECTOR : ALPHA_TRANSPARENT); + + mPaintSelector[MINUTES][SELECTOR_CIRCLE].setAlpha( + mShowHours ? ALPHA_TRANSPARENT : ALPHA_SELECTOR); + mPaintSelector[MINUTES][SELECTOR_DOT].setAlpha( + mShowHours ? ALPHA_TRANSPARENT : ALPHA_OPAQUE); + mPaintSelector[MINUTES][SELECTOR_LINE].setAlpha( + mShowHours ? ALPHA_TRANSPARENT : ALPHA_SELECTOR); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + updateLayoutData(); + } + + private void updateLayoutData() { + mXCenter = getWidth() / 2; + mYCenter = getHeight() / 2; + + final int min = Math.min(mXCenter, mYCenter); + + mCircleRadius[HOURS] = min * mCircleRadiusMultiplier[HOURS]; + mCircleRadius[HOURS_INNER] = min * mCircleRadiusMultiplier[HOURS]; + mCircleRadius[MINUTES] = min * mCircleRadiusMultiplier[MINUTES]; + + if (!mIs24HourMode) { + // We'll need to draw the AM/PM circles, so the main circle will need to have + // a slightly higher center. To keep the entire view centered vertically, we'll + // have to push it up by half the radius of the AM/PM circles. + int amPmCircleRadius = (int) (mCircleRadius[HOURS] * mAmPmCircleRadiusMultiplier); + mYCenter -= amPmCircleRadius / 2; + } + + mMinHypotenuseForInnerNumber = (int) (mCircleRadius[HOURS] + * mNumbersRadiusMultiplier[HOURS_INNER]) - mSelectionRadius[HOURS]; + mMaxHypotenuseForOuterNumber = (int) (mCircleRadius[HOURS] + * mNumbersRadiusMultiplier[HOURS]) + mSelectionRadius[HOURS]; + mHalfwayHypotenusePoint = (int) (mCircleRadius[HOURS] + * ((mNumbersRadiusMultiplier[HOURS] + mNumbersRadiusMultiplier[HOURS_INNER]) / 2)); + + mTextSize[HOURS] = mCircleRadius[HOURS] * mTextSizeMultiplier[HOURS]; + mTextSize[MINUTES] = mCircleRadius[MINUTES] * mTextSizeMultiplier[MINUTES]; + + if (mIs24HourMode) { + mInnerTextSize = mCircleRadius[HOURS] * mTextSizeMultiplier[HOURS_INNER]; + } + + calculateGridSizesHours(); + calculateGridSizesMinutes(); + + mSelectionRadius[HOURS] = (int) (mCircleRadius[HOURS] * mSelectionRadiusMultiplier); + mSelectionRadius[HOURS_INNER] = mSelectionRadius[HOURS]; + mSelectionRadius[MINUTES] = (int) (mCircleRadius[MINUTES] * mSelectionRadiusMultiplier); + + mAmPmCircleRadius = (int) (mCircleRadius[HOURS] * mAmPmCircleRadiusMultiplier); + mPaintAmPmText.setTextSize(mAmPmCircleRadius * 3 / 4); + + // Line up the vertical center of the AM/PM circles with the bottom of the main circle. + mAmPmYCenter = mYCenter + mCircleRadius[HOURS]; + + // Line up the horizontal edges of the AM/PM circles with the horizontal edges + // of the main circle + mLeftIndicatorXCenter = mXCenter - mCircleRadius[HOURS] + mAmPmCircleRadius; + mRightIndicatorXCenter = mXCenter + mCircleRadius[HOURS] - mAmPmCircleRadius; + } + + @Override + public void onDraw(Canvas canvas) { + canvas.save(); + + calculateGridSizesHours(); + calculateGridSizesMinutes(); + + drawCircleBackground(canvas); + + drawTextElements(canvas, mTextSize[HOURS], mTypeface, mOuterTextHours, + mTextGridWidths[HOURS], mTextGridHeights[HOURS], mPaint[HOURS]); + + if (mIs24HourMode && mInnerTextHours != null) { + drawTextElements(canvas, mInnerTextSize, mTypeface, mInnerTextHours, + mInnerTextGridWidths, mInnerTextGridHeights, mPaint[HOURS]); + } + + drawTextElements(canvas, mTextSize[MINUTES], mTypeface, mOuterTextMinutes, + mTextGridWidths[MINUTES], mTextGridHeights[MINUTES], mPaint[MINUTES]); + + drawCenter(canvas); + drawSelector(canvas); + if (!mIs24HourMode) { + drawAmPm(canvas); + } + + if(!mInputEnabled) { + // Draw outer view rectangle + mRectF.set(0, 0, getWidth(), getHeight()); + canvas.drawRect(mRectF, mPaintDisabled); + } + + if (DEBUG) { + drawDebug(canvas); + } + + canvas.restore(); + } + + private void drawCircleBackground(Canvas canvas) { + canvas.drawCircle(mXCenter, mYCenter, mCircleRadius[HOURS], mPaintBackground); + } + + private void drawCenter(Canvas canvas) { + canvas.drawCircle(mXCenter, mYCenter, CENTER_RADIUS, mPaintCenter); + } + + private void drawSelector(Canvas canvas) { + drawSelector(canvas, mIsOnInnerCircle ? HOURS_INNER : HOURS); + drawSelector(canvas, MINUTES); + } + + private void drawAmPm(Canvas canvas) { + final boolean isLayoutRtl = isLayoutRtl(); + + int amColor = mAmPmUnselectedColor; + int amAlpha = ALPHA_OPAQUE; + int pmColor = mAmPmUnselectedColor; + int pmAlpha = ALPHA_OPAQUE; + if (mAmOrPm == AM) { + amColor = mAmPmSelectedColor; + amAlpha = ALPHA_AMPM_SELECTED; + } else if (mAmOrPm == PM) { + pmColor = mAmPmSelectedColor; + pmAlpha = ALPHA_AMPM_SELECTED; + } + if (mAmOrPmPressed == AM) { + amColor = mAmPmSelectedColor; + amAlpha = ALPHA_AMPM_PRESSED; + } else if (mAmOrPmPressed == PM) { + pmColor = mAmPmSelectedColor; + pmAlpha = ALPHA_AMPM_PRESSED; + } + + // Draw the two circles + mPaintAmPmCircle[AM].setColor(amColor); + mPaintAmPmCircle[AM].setAlpha(amAlpha); + canvas.drawCircle(isLayoutRtl ? mRightIndicatorXCenter : mLeftIndicatorXCenter, + mAmPmYCenter, mAmPmCircleRadius, mPaintAmPmCircle[AM]); + + mPaintAmPmCircle[PM].setColor(pmColor); + mPaintAmPmCircle[PM].setAlpha(pmAlpha); + canvas.drawCircle(isLayoutRtl ? mLeftIndicatorXCenter : mRightIndicatorXCenter, + mAmPmYCenter, mAmPmCircleRadius, mPaintAmPmCircle[PM]); + + // Draw the AM/PM texts on top + mPaintAmPmText.setColor(mAmPmTextColor); + float textYCenter = mAmPmYCenter - + (int) (mPaintAmPmText.descent() + mPaintAmPmText.ascent()) / 2; + + canvas.drawText(isLayoutRtl ? mAmPmText[PM] : mAmPmText[AM], mLeftIndicatorXCenter, + textYCenter, mPaintAmPmText); + canvas.drawText(isLayoutRtl ? mAmPmText[AM] : mAmPmText[PM], mRightIndicatorXCenter, + textYCenter, mPaintAmPmText); + } + + private void drawSelector(Canvas canvas, int index) { + // Calculate the current radius at which to place the selection circle. + mLineLength[index] = (int) (mCircleRadius[index] + * mNumbersRadiusMultiplier[index] * mAnimationRadiusMultiplier[index]); + + double selectionRadians = Math.toRadians(mSelectionDegrees[index]); + + int pointX = mXCenter + (int) (mLineLength[index] * Math.sin(selectionRadians)); + int pointY = mYCenter - (int) (mLineLength[index] * Math.cos(selectionRadians)); + + // Draw the selection circle + canvas.drawCircle(pointX, pointY, mSelectionRadius[index], + mPaintSelector[index % 2][SELECTOR_CIRCLE]); + + // Draw the dot if needed + if (mSelectionDegrees[index] % 30 != 0) { + // We're not on a direct tick + canvas.drawCircle(pointX, pointY, (mSelectionRadius[index] * 2 / 7), + mPaintSelector[index % 2][SELECTOR_DOT]); + } else { + // We're not drawing the dot, so shorten the line to only go as far as the edge of the + // selection circle + int lineLength = mLineLength[index] - mSelectionRadius[index]; + pointX = mXCenter + (int) (lineLength * Math.sin(selectionRadians)); + pointY = mYCenter - (int) (lineLength * Math.cos(selectionRadians)); + } + + // Draw the line + canvas.drawLine(mXCenter, mYCenter, pointX, pointY, + mPaintSelector[index % 2][SELECTOR_LINE]); + } + + private void drawDebug(Canvas canvas) { + // Draw outer numbers circle + final float outerRadius = mCircleRadius[HOURS] * mNumbersRadiusMultiplier[HOURS]; + canvas.drawCircle(mXCenter, mYCenter, outerRadius, mPaintDebug); + + // Draw inner numbers circle + final float innerRadius = mCircleRadius[HOURS] * mNumbersRadiusMultiplier[HOURS_INNER]; + canvas.drawCircle(mXCenter, mYCenter, innerRadius, mPaintDebug); + + // Draw outer background circle + canvas.drawCircle(mXCenter, mYCenter, mCircleRadius[HOURS], mPaintDebug); + + // Draw outer rectangle for circles + float left = mXCenter - outerRadius; + float top = mYCenter - outerRadius; + float right = mXCenter + outerRadius; + float bottom = mYCenter + outerRadius; + mRectF = new RectF(left, top, right, bottom); + canvas.drawRect(mRectF, mPaintDebug); + + // Draw outer rectangle for background + left = mXCenter - mCircleRadius[HOURS]; + top = mYCenter - mCircleRadius[HOURS]; + right = mXCenter + mCircleRadius[HOURS]; + bottom = mYCenter + mCircleRadius[HOURS]; + mRectF.set(left, top, right, bottom); + canvas.drawRect(mRectF, mPaintDebug); + + // Draw outer view rectangle + mRectF.set(0, 0, getWidth(), getHeight()); + canvas.drawRect(mRectF, mPaintDebug); + + // Draw selected time + final String selected = String.format("%02d:%02d", getCurrentHour(), getCurrentMinute()); + + ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + TextView tv = new TextView(getContext()); + tv.setLayoutParams(lp); + tv.setText(selected); + tv.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + Paint paint = tv.getPaint(); + paint.setColor(DEBUG_TEXT_COLOR); + + final int width = tv.getMeasuredWidth(); + + float height = paint.descent() - paint.ascent(); + float x = mXCenter - width / 2; + float y = mYCenter + 1.5f * height; + + canvas.drawText(selected.toString(), x, y, paint); + } + + private void calculateGridSizesHours() { + // Calculate the text positions + float numbersRadius = mCircleRadius[HOURS] + * mNumbersRadiusMultiplier[HOURS] * mAnimationRadiusMultiplier[HOURS]; + + // Calculate the positions for the 12 numbers in the main circle. + calculateGridSizes(mPaint[HOURS], numbersRadius, mXCenter, mYCenter, + mTextSize[HOURS], mTextGridHeights[HOURS], mTextGridWidths[HOURS]); + + // If we have an inner circle, calculate those positions too. + if (mIs24HourMode) { + float innerNumbersRadius = mCircleRadius[HOURS_INNER] + * mNumbersRadiusMultiplier[HOURS_INNER] + * mAnimationRadiusMultiplier[HOURS_INNER]; + + calculateGridSizes(mPaint[HOURS], innerNumbersRadius, mXCenter, mYCenter, + mInnerTextSize, mInnerTextGridHeights, mInnerTextGridWidths); + } + } + + private void calculateGridSizesMinutes() { + // Calculate the text positions + float numbersRadius = mCircleRadius[MINUTES] + * mNumbersRadiusMultiplier[MINUTES] * mAnimationRadiusMultiplier[MINUTES]; + + // Calculate the positions for the 12 numbers in the main circle. + calculateGridSizes(mPaint[MINUTES], numbersRadius, mXCenter, mYCenter, + mTextSize[MINUTES], mTextGridHeights[MINUTES], mTextGridWidths[MINUTES]); + } + + + /** + * Using the trigonometric Unit Circle, calculate the positions that the text will need to be + * drawn at based on the specified circle radius. Place the values in the textGridHeights and + * textGridWidths parameters. + */ + private static void calculateGridSizes(Paint paint, float numbersRadius, float xCenter, + float yCenter, float textSize, float[] textGridHeights, float[] textGridWidths) { + /* + * The numbers need to be drawn in a 7x7 grid, representing the points on the Unit Circle. + */ + final float offset1 = numbersRadius; + // cos(30) = a / r => r * cos(30) + final float offset2 = numbersRadius * COSINE_30_DEGREES; + // sin(30) = o / r => r * sin(30) + final float offset3 = numbersRadius * SINE_30_DEGREES; + + paint.setTextSize(textSize); + // We'll need yTextBase to be slightly lower to account for the text's baseline. + yCenter -= (paint.descent() + paint.ascent()) / 2; + + textGridHeights[0] = yCenter - offset1; + textGridWidths[0] = xCenter - offset1; + textGridHeights[1] = yCenter - offset2; + textGridWidths[1] = xCenter - offset2; + textGridHeights[2] = yCenter - offset3; + textGridWidths[2] = xCenter - offset3; + textGridHeights[3] = yCenter; + textGridWidths[3] = xCenter; + textGridHeights[4] = yCenter + offset3; + textGridWidths[4] = xCenter + offset3; + textGridHeights[5] = yCenter + offset2; + textGridWidths[5] = xCenter + offset2; + textGridHeights[6] = yCenter + offset1; + textGridWidths[6] = xCenter + offset1; + } + + /** + * Draw the 12 text values at the positions specified by the textGrid parameters. + */ + private void drawTextElements(Canvas canvas, float textSize, Typeface typeface, String[] texts, + float[] textGridWidths, float[] textGridHeights, Paint paint) { + paint.setTextSize(textSize); + paint.setTypeface(typeface); + canvas.drawText(texts[0], textGridWidths[3], textGridHeights[0], paint); + canvas.drawText(texts[1], textGridWidths[4], textGridHeights[1], paint); + canvas.drawText(texts[2], textGridWidths[5], textGridHeights[2], paint); + canvas.drawText(texts[3], textGridWidths[6], textGridHeights[3], paint); + canvas.drawText(texts[4], textGridWidths[5], textGridHeights[4], paint); + canvas.drawText(texts[5], textGridWidths[4], textGridHeights[5], paint); + canvas.drawText(texts[6], textGridWidths[3], textGridHeights[6], paint); + canvas.drawText(texts[7], textGridWidths[2], textGridHeights[5], paint); + canvas.drawText(texts[8], textGridWidths[1], textGridHeights[4], paint); + canvas.drawText(texts[9], textGridWidths[0], textGridHeights[3], paint); + canvas.drawText(texts[10], textGridWidths[1], textGridHeights[2], paint); + canvas.drawText(texts[11], textGridWidths[2], textGridHeights[1], paint); + } + + // Used for animating the hours by changing their radius + private void setAnimationRadiusMultiplierHours(float animationRadiusMultiplier) { + mAnimationRadiusMultiplier[HOURS] = animationRadiusMultiplier; + mAnimationRadiusMultiplier[HOURS_INNER] = animationRadiusMultiplier; + } + + // Used for animating the minutes by changing their radius + private void setAnimationRadiusMultiplierMinutes(float animationRadiusMultiplier) { + mAnimationRadiusMultiplier[MINUTES] = animationRadiusMultiplier; + } + + private static ObjectAnimator getRadiusDisappearAnimator(Object target, + String radiusPropertyName, InvalidateUpdateListener updateListener, + float midRadiusMultiplier, float endRadiusMultiplier) { + Keyframe kf0, kf1, kf2; + float midwayPoint = 0.2f; + int duration = 500; + + kf0 = Keyframe.ofFloat(0f, 1); + kf1 = Keyframe.ofFloat(midwayPoint, midRadiusMultiplier); + kf2 = Keyframe.ofFloat(1f, endRadiusMultiplier); + PropertyValuesHolder radiusDisappear = PropertyValuesHolder.ofKeyframe( + radiusPropertyName, kf0, kf1, kf2); + + ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder( + target, radiusDisappear).setDuration(duration); + animator.addUpdateListener(updateListener); + return animator; + } + + private static ObjectAnimator getRadiusReappearAnimator(Object target, + String radiusPropertyName, InvalidateUpdateListener updateListener, + float midRadiusMultiplier, float endRadiusMultiplier) { + Keyframe kf0, kf1, kf2, kf3; + float midwayPoint = 0.2f; + int duration = 500; + + // Set up animator for reappearing. + float delayMultiplier = 0.25f; + float transitionDurationMultiplier = 1f; + float totalDurationMultiplier = transitionDurationMultiplier + delayMultiplier; + int totalDuration = (int) (duration * totalDurationMultiplier); + float delayPoint = (delayMultiplier * duration) / totalDuration; + midwayPoint = 1 - (midwayPoint * (1 - delayPoint)); + + kf0 = Keyframe.ofFloat(0f, endRadiusMultiplier); + kf1 = Keyframe.ofFloat(delayPoint, endRadiusMultiplier); + kf2 = Keyframe.ofFloat(midwayPoint, midRadiusMultiplier); + kf3 = Keyframe.ofFloat(1f, 1); + PropertyValuesHolder radiusReappear = PropertyValuesHolder.ofKeyframe( + radiusPropertyName, kf0, kf1, kf2, kf3); + + ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder( + target, radiusReappear).setDuration(totalDuration); + animator.addUpdateListener(updateListener); + return animator; + } + + private static ObjectAnimator getFadeOutAnimator(Object target, int startAlpha, int endAlpha, + InvalidateUpdateListener updateListener) { + int duration = 500; + ObjectAnimator animator = ObjectAnimator.ofInt(target, "alpha", startAlpha, endAlpha); + animator.setDuration(duration); + animator.addUpdateListener(updateListener); + + return animator; + } + + private static ObjectAnimator getFadeInAnimator(Object target, int startAlpha, int endAlpha, + InvalidateUpdateListener updateListener) { + Keyframe kf0, kf1, kf2; + int duration = 500; + + // Set up animator for reappearing. + float delayMultiplier = 0.25f; + float transitionDurationMultiplier = 1f; + float totalDurationMultiplier = transitionDurationMultiplier + delayMultiplier; + int totalDuration = (int) (duration * totalDurationMultiplier); + float delayPoint = (delayMultiplier * duration) / totalDuration; + + kf0 = Keyframe.ofInt(0f, startAlpha); + kf1 = Keyframe.ofInt(delayPoint, startAlpha); + kf2 = Keyframe.ofInt(1f, endAlpha); + PropertyValuesHolder fadeIn = PropertyValuesHolder.ofKeyframe("alpha", kf0, kf1, kf2); + + ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder( + target, fadeIn).setDuration(totalDuration); + animator.addUpdateListener(updateListener); + return animator; + } + + private class InvalidateUpdateListener implements ValueAnimator.AnimatorUpdateListener { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + RadialTimePickerView.this.invalidate(); + } + } + + private void startHoursToMinutesAnimation() { + if (mHoursToMinutesAnims.size() == 0) { + mHoursToMinutesAnims.add(getRadiusDisappearAnimator(this, + "animationRadiusMultiplierHours", mInvalidateUpdateListener, + mTransitionMidRadiusMultiplier, mTransitionEndRadiusMultiplier)); + mHoursToMinutesAnims.add(getFadeOutAnimator(mPaint[HOURS], + ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); + mHoursToMinutesAnims.add(getFadeOutAnimator(mPaintSelector[HOURS][SELECTOR_CIRCLE], + ALPHA_SELECTOR, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); + mHoursToMinutesAnims.add(getFadeOutAnimator(mPaintSelector[HOURS][SELECTOR_DOT], + ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); + mHoursToMinutesAnims.add(getFadeOutAnimator(mPaintSelector[HOURS][SELECTOR_LINE], + ALPHA_SELECTOR, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); + + mHoursToMinutesAnims.add(getRadiusReappearAnimator(this, + "animationRadiusMultiplierMinutes", mInvalidateUpdateListener, + mTransitionMidRadiusMultiplier, mTransitionEndRadiusMultiplier)); + mHoursToMinutesAnims.add(getFadeInAnimator(mPaint[MINUTES], + ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); + mHoursToMinutesAnims.add(getFadeInAnimator(mPaintSelector[MINUTES][SELECTOR_CIRCLE], + ALPHA_TRANSPARENT, ALPHA_SELECTOR, mInvalidateUpdateListener)); + mHoursToMinutesAnims.add(getFadeInAnimator(mPaintSelector[MINUTES][SELECTOR_DOT], + ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); + mHoursToMinutesAnims.add(getFadeInAnimator(mPaintSelector[MINUTES][SELECTOR_LINE], + ALPHA_TRANSPARENT, ALPHA_SELECTOR, mInvalidateUpdateListener)); + } + + if (mTransition != null && mTransition.isRunning()) { + mTransition.end(); + } + mTransition = new AnimatorSet(); + mTransition.playTogether(mHoursToMinutesAnims); + mTransition.start(); + } + + private void startMinutesToHoursAnimation() { + if (mMinuteToHoursAnims.size() == 0) { + mMinuteToHoursAnims.add(getRadiusDisappearAnimator(this, + "animationRadiusMultiplierMinutes", mInvalidateUpdateListener, + mTransitionMidRadiusMultiplier, mTransitionEndRadiusMultiplier)); + mMinuteToHoursAnims.add(getFadeOutAnimator(mPaint[MINUTES], + ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); + mMinuteToHoursAnims.add(getFadeOutAnimator(mPaintSelector[MINUTES][SELECTOR_CIRCLE], + ALPHA_SELECTOR, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); + mMinuteToHoursAnims.add(getFadeOutAnimator(mPaintSelector[MINUTES][SELECTOR_DOT], + ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); + mMinuteToHoursAnims.add(getFadeOutAnimator(mPaintSelector[MINUTES][SELECTOR_LINE], + ALPHA_SELECTOR, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); + + mMinuteToHoursAnims.add(getRadiusReappearAnimator(this, + "animationRadiusMultiplierHours", mInvalidateUpdateListener, + mTransitionMidRadiusMultiplier, mTransitionEndRadiusMultiplier)); + mMinuteToHoursAnims.add(getFadeInAnimator(mPaint[HOURS], + ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); + mMinuteToHoursAnims.add(getFadeInAnimator(mPaintSelector[HOURS][SELECTOR_CIRCLE], + ALPHA_TRANSPARENT, ALPHA_SELECTOR, mInvalidateUpdateListener)); + mMinuteToHoursAnims.add(getFadeInAnimator(mPaintSelector[HOURS][SELECTOR_DOT], + ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); + mMinuteToHoursAnims.add(getFadeInAnimator(mPaintSelector[HOURS][SELECTOR_LINE], + ALPHA_TRANSPARENT, ALPHA_SELECTOR, mInvalidateUpdateListener)); + } + + if (mTransition != null && mTransition.isRunning()) { + mTransition.end(); + } + mTransition = new AnimatorSet(); + mTransition.playTogether(mMinuteToHoursAnims); + mTransition.start(); + } + + private int getDegreesFromXY(float x, float y) { + final double hypotenuse = Math.sqrt( + (y - mYCenter) * (y - mYCenter) + (x - mXCenter) * (x - mXCenter)); + + // Basic check if we're outside the range of the disk + if (hypotenuse > mCircleRadius[HOURS]) { + return -1; + } + // Check + if (mIs24HourMode && mShowHours) { + if (hypotenuse >= mMinHypotenuseForInnerNumber + && hypotenuse <= mHalfwayHypotenusePoint) { + mIsOnInnerCircle = true; + } else if (hypotenuse <= mMaxHypotenuseForOuterNumber + && hypotenuse >= mHalfwayHypotenusePoint) { + mIsOnInnerCircle = false; + } else { + return -1; + } + } else { + final int index = (mShowHours) ? HOURS : MINUTES; + final float length = (mCircleRadius[index] * mNumbersRadiusMultiplier[index]); + final int distanceToNumber = (int) Math.abs(hypotenuse - length); + final int maxAllowedDistance = + (int) (mCircleRadius[index] * (1 - mNumbersRadiusMultiplier[index])); + if (distanceToNumber > maxAllowedDistance) { + return -1; + } + } + + final float opposite = Math.abs(y - mYCenter); + double degrees = Math.toDegrees(Math.asin(opposite / hypotenuse)); + + // Now we have to translate to the correct quadrant. + boolean rightSide = (x > mXCenter); + boolean topSide = (y < mYCenter); + if (rightSide && topSide) { + degrees = 90 - degrees; + } else if (rightSide && !topSide) { + degrees = 90 + degrees; + } else if (!rightSide && !topSide) { + degrees = 270 - degrees; + } else if (!rightSide && topSide) { + degrees = 270 + degrees; + } + return (int) degrees; + } + + private int getIsTouchingAmOrPm(float x, float y) { + final boolean isLayoutRtl = isLayoutRtl(); + int squaredYDistance = (int) ((y - mAmPmYCenter) * (y - mAmPmYCenter)); + + int distanceToAmCenter = (int) Math.sqrt( + (x - mLeftIndicatorXCenter) * (x - mLeftIndicatorXCenter) + squaredYDistance); + if (distanceToAmCenter <= mAmPmCircleRadius) { + return (isLayoutRtl ? PM : AM); + } + + int distanceToPmCenter = (int) Math.sqrt( + (x - mRightIndicatorXCenter) * (x - mRightIndicatorXCenter) + squaredYDistance); + if (distanceToPmCenter <= mAmPmCircleRadius) { + return (isLayoutRtl ? AM : PM); + } + + // Neither was close enough. + return -1; + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + if(!mInputEnabled) { + return true; + } + + final float eventX = event.getX(); + final float eventY = event.getY(); + + int degrees; + int snapDegrees; + boolean result = false; + + switch(event.getAction()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: + mAmOrPmPressed = getIsTouchingAmOrPm(eventX, eventY); + if (mAmOrPmPressed != -1) { + result = true; + } else { + degrees = getDegreesFromXY(eventX, eventY); + if (degrees != -1) { + snapDegrees = (mShowHours ? + snapOnly30s(degrees, 0) : snapPrefer30s(degrees)) % 360; + if (mShowHours) { + mSelectionDegrees[HOURS] = snapDegrees; + mSelectionDegrees[HOURS_INNER] = snapDegrees; + } else { + mSelectionDegrees[MINUTES] = snapDegrees; + } + performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK); + if (mListener != null) { + if (mShowHours) { + mListener.onValueSelected(HOURS, getCurrentHour(), false); + } else { + mListener.onValueSelected(MINUTES, getCurrentMinute(), false); + } + } + result = true; + } + } + invalidate(); + return result; + + case MotionEvent.ACTION_UP: + mAmOrPmPressed = getIsTouchingAmOrPm(eventX, eventY); + if (mAmOrPmPressed != -1) { + if (mAmOrPm != mAmOrPmPressed) { + swapAmPm(); + } + mAmOrPmPressed = -1; + if (mListener != null) { + mListener.onValueSelected(AMPM, getCurrentHour(), true); + } + result = true; + } else { + degrees = getDegreesFromXY(eventX, eventY); + if (degrees != -1) { + snapDegrees = (mShowHours ? + snapOnly30s(degrees, 0) : snapPrefer30s(degrees)) % 360; + if (mShowHours) { + mSelectionDegrees[HOURS] = snapDegrees; + mSelectionDegrees[HOURS_INNER] = snapDegrees; + } else { + mSelectionDegrees[MINUTES] = snapDegrees; + } + if (mListener != null) { + if (mShowHours) { + mListener.onValueSelected(HOURS, getCurrentHour(), true); + } else { + mListener.onValueSelected(MINUTES, getCurrentMinute(), true); + } + } + result = true; + } + } + if (result) { + invalidate(); + } + return result; + + default: + break; + } + return false; + } + + /** + * Necessary for accessibility, to ensure we support "scrolling" forward and backward + * in the circle. + */ + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); + } + + /** + * Announce the currently-selected time when launched. + */ + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { + // Clear the event's current text so that only the current time will be spoken. + event.getText().clear(); + Time time = new Time(); + time.hour = getCurrentHour(); + time.minute = getCurrentMinute(); + long millis = time.normalize(true); + int flags = DateUtils.FORMAT_SHOW_TIME; + if (mIs24HourMode) { + flags |= DateUtils.FORMAT_24HOUR; + } + String timeString = DateUtils.formatDateTime(getContext(), millis, flags); + event.getText().add(timeString); + return true; + } + return super.dispatchPopulateAccessibilityEvent(event); + } + + /** + * When scroll forward/backward events are received, jump the time to the higher/lower + * discrete, visible value on the circle. + */ + @SuppressLint("NewApi") + @Override + public boolean performAccessibilityAction(int action, Bundle arguments) { + if (super.performAccessibilityAction(action, arguments)) { + return true; + } + + int changeMultiplier = 0; + if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) { + changeMultiplier = 1; + } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) { + changeMultiplier = -1; + } + if (changeMultiplier != 0) { + int value = 0; + int stepSize = 0; + if (mShowHours) { + stepSize = DEGREES_FOR_ONE_HOUR; + value = getCurrentHour() % 12; + } else { + stepSize = DEGREES_FOR_ONE_MINUTE; + value = getCurrentMinute(); + } + + int degrees = value * stepSize; + degrees = snapOnly30s(degrees, changeMultiplier); + value = degrees / stepSize; + int maxValue = 0; + int minValue = 0; + if (mShowHours) { + if (mIs24HourMode) { + maxValue = 23; + } else { + maxValue = 12; + minValue = 1; + } + } else { + maxValue = 55; + } + if (value > maxValue) { + // If we scrolled forward past the highest number, wrap around to the lowest. + value = minValue; + } else if (value < minValue) { + // If we scrolled backward past the lowest number, wrap around to the highest. + value = maxValue; + } + if (mShowHours) { + setCurrentHour(value); + if (mListener != null) { + mListener.onValueSelected(HOURS, value, false); + } + } else { + setCurrentMinute(value); + if (mListener != null) { + mListener.onValueSelected(MINUTES, value, false); + } + } + return true; + } + + return false; + } + + public void setInputEnabled(boolean inputEnabled) { + mInputEnabled = inputEnabled; + invalidate(); + } +} diff --git a/core/java/android/widget/RadioButton.java b/core/java/android/widget/RadioButton.java index a0fef7d..afc4830 100644 --- a/core/java/android/widget/RadioButton.java +++ b/core/java/android/widget/RadioButton.java @@ -21,8 +21,6 @@ import android.util.AttributeSet; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; -import com.android.internal.R; - /** * <p> @@ -59,8 +57,12 @@ public class RadioButton extends CompoundButton { this(context, attrs, com.android.internal.R.attr.radioButtonStyle); } - public RadioButton(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public RadioButton(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public RadioButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); } /** diff --git a/core/java/android/widget/RatingBar.java b/core/java/android/widget/RatingBar.java index 4d3c56c..82b490e 100644 --- a/core/java/android/widget/RatingBar.java +++ b/core/java/android/widget/RatingBar.java @@ -82,11 +82,15 @@ public class RatingBar extends AbsSeekBar { private OnRatingBarChangeListener mOnRatingBarChangeListener; - public RatingBar(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RatingBar, - defStyle, 0); + public RatingBar(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public RatingBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + final TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.RatingBar, defStyleAttr, defStyleRes); final int numStars = a.getInt(R.styleable.RatingBar_numStars, mNumStars); setIsIndicator(a.getBoolean(R.styleable.RatingBar_isIndicator, !mIsUserSeekable)); final float rating = a.getFloat(R.styleable.RatingBar_rating, -1); diff --git a/core/java/android/widget/RelativeLayout.java b/core/java/android/widget/RelativeLayout.java index e03e83d..90e80d3 100644 --- a/core/java/android/widget/RelativeLayout.java +++ b/core/java/android/widget/RelativeLayout.java @@ -228,24 +228,27 @@ public class RelativeLayout extends ViewGroup { private static final int DEFAULT_WIDTH = 0x00010000; public RelativeLayout(Context context) { - super(context); - queryCompatibilityModes(context); + this(context, null); } public RelativeLayout(Context context, AttributeSet attrs) { - super(context, attrs); - initFromAttributes(context, attrs); - queryCompatibilityModes(context); + this(context, attrs, 0); + } + + public RelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); } - public RelativeLayout(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - initFromAttributes(context, attrs); + public RelativeLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + initFromAttributes(context, attrs, defStyleAttr, defStyleRes); queryCompatibilityModes(context); } - private void initFromAttributes(Context context, AttributeSet attrs) { - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RelativeLayout); + private void initFromAttributes( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + final TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.RelativeLayout, defStyleAttr, defStyleRes); mIgnoreGravity = a.getResourceId(R.styleable.RelativeLayout_ignoreGravity, View.NO_ID); mGravity = a.getInt(R.styleable.RelativeLayout_gravity, mGravity); a.recycle(); @@ -738,18 +741,29 @@ public class RelativeLayout extends ViewGroup { private int getChildMeasureSpec(int childStart, int childEnd, int childSize, int startMargin, int endMargin, int startPadding, int endPadding, int mySize) { + int childSpecMode = 0; + int childSpecSize = 0; + + // Negative values in a mySize/myWidth/myWidth value in RelativeLayout + // measurement is code for, "we got an unspecified mode in the + // RelativeLayout's measure spec." if (mySize < 0 && !mAllowBrokenMeasureSpecs) { - if (childSize >= 0) { - return MeasureSpec.makeMeasureSpec(childSize, MeasureSpec.EXACTLY); + if (childStart >= 0 && childEnd >= 0) { + // Constraints fixed both edges, so child has an exact size. + childSpecSize = Math.max(0, childEnd - childStart); + childSpecMode = MeasureSpec.EXACTLY; + } else if (childSize >= 0) { + // The child specified an exact size. + childSpecSize = childSize; + childSpecMode = MeasureSpec.EXACTLY; + } else { + // Allow the child to be whatever size it wants. + childSpecSize = 0; + childSpecMode = MeasureSpec.UNSPECIFIED; } - // Negative values in a mySize/myWidth/myWidth value in RelativeLayout measurement - // is code for, "we got an unspecified mode in the RelativeLayout's measurespec." - // Carry it forward. - return MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); - } - int childSpecMode = 0; - int childSpecSize = 0; + return MeasureSpec.makeMeasureSpec(childSpecSize, childSpecMode); + } // Figure out start and end bounds. int tempStart = childStart; diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index 0d3df51..f7d20b53 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -1516,6 +1516,75 @@ public class RemoteViews implements Parcelable, Filter { } /** + * Helper action to set a color filter on a compound drawable on a TextView. Supports relative + * (s/t/e/b) or cardinal (l/t/r/b) arrangement. + */ + private class TextViewDrawableColorFilterAction extends Action { + public TextViewDrawableColorFilterAction(int viewId, boolean isRelative, int index, + int color, PorterDuff.Mode mode) { + this.viewId = viewId; + this.isRelative = isRelative; + this.index = index; + this.color = color; + this.mode = mode; + } + + public TextViewDrawableColorFilterAction(Parcel parcel) { + viewId = parcel.readInt(); + isRelative = (parcel.readInt() != 0); + index = parcel.readInt(); + color = parcel.readInt(); + mode = readPorterDuffMode(parcel); + } + + private PorterDuff.Mode readPorterDuffMode(Parcel parcel) { + int mode = parcel.readInt(); + if (mode >= 0 && mode < PorterDuff.Mode.values().length) { + return PorterDuff.Mode.values()[mode]; + } else { + return PorterDuff.Mode.CLEAR; + } + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(TAG); + dest.writeInt(viewId); + dest.writeInt(isRelative ? 1 : 0); + dest.writeInt(index); + dest.writeInt(color); + dest.writeInt(mode.ordinal()); + } + + @Override + public void apply(View root, ViewGroup rootParent, OnClickHandler handler) { + final TextView target = (TextView) root.findViewById(viewId); + if (target == null) return; + Drawable[] drawables = isRelative + ? target.getCompoundDrawablesRelative() + : target.getCompoundDrawables(); + if (index < 0 || index >= 4) { + throw new IllegalStateException("index must be in range [0, 3]."); + } + Drawable d = drawables[index]; + if (d != null) { + d.mutate(); + d.setColorFilter(color, mode); + } + } + + public String getActionName() { + return "TextViewDrawableColorFilterAction"; + } + + final boolean isRelative; + final int index; + final int color; + final PorterDuff.Mode mode; + + public final static int TAG = 17; + } + + /** * Simple class used to keep track of memory usage in a RemoteViews. * */ @@ -1686,6 +1755,9 @@ public class RemoteViews implements Parcelable, Filter { case SetRemoteViewsAdapterList.TAG: mActions.add(new SetRemoteViewsAdapterList(parcel)); break; + case TextViewDrawableColorFilterAction.TAG: + mActions.add(new TextViewDrawableColorFilterAction(parcel)); + break; default: throw new ActionException("Tag " + tag + " not found"); } @@ -1921,6 +1993,28 @@ public class RemoteViews implements Parcelable, Filter { } /** + * Equivalent to applying a color filter on one of the drawables in + * {@link android.widget.TextView#getCompoundDrawablesRelative()}. + * + * @param viewId The id of the view whose text should change. + * @param index The index of the drawable in the array of + * {@link android.widget.TextView#getCompoundDrawablesRelative()} to set the color + * filter on. Must be in [0, 3]. + * @param color The color of the color filter. See + * {@link Drawable#setColorFilter(int, android.graphics.PorterDuff.Mode)}. + * @param mode The mode of the color filter. See + * {@link Drawable#setColorFilter(int, android.graphics.PorterDuff.Mode)}. + * @hide + */ + public void setTextViewCompoundDrawablesRelativeColorFilter(int viewId, + int index, int color, PorterDuff.Mode mode) { + if (index < 0 || index >= 4) { + throw new IllegalArgumentException("index must be in range [0, 3]."); + } + addAction(new TextViewDrawableColorFilterAction(viewId, true, index, color, mode)); + } + + /** * Equivalent to calling ImageView.setImageResource * * @param viewId The id of the view whose drawable should change diff --git a/core/java/android/widget/RemoteViewsAdapter.java b/core/java/android/widget/RemoteViewsAdapter.java index 3ff0cee..bbe6f9e 100644 --- a/core/java/android/widget/RemoteViewsAdapter.java +++ b/core/java/android/widget/RemoteViewsAdapter.java @@ -32,7 +32,6 @@ import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.Message; -import android.os.Process; import android.os.RemoteException; import android.os.UserHandle; import android.util.Log; @@ -45,7 +44,6 @@ import android.widget.RemoteViews.OnClickHandler; import com.android.internal.widget.IRemoteViewsAdapterConnection; import com.android.internal.widget.IRemoteViewsFactory; -import com.android.internal.widget.LockPatternUtils; /** * An adapter to a RemoteViewsService which fetches and caches RemoteViews diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java index 6680393..082d728 100644 --- a/core/java/android/widget/ScrollView.java +++ b/core/java/android/widget/ScrollView.java @@ -162,12 +162,16 @@ public class ScrollView extends FrameLayout { this(context, attrs, com.android.internal.R.attr.scrollViewStyle); } - public ScrollView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public ScrollView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); initScrollView(); - TypedArray a = - context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.ScrollView, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.ScrollView, defStyleAttr, defStyleRes); setFillViewport(a.getBoolean(R.styleable.ScrollView_fillViewport, false)); diff --git a/core/java/android/widget/Scroller.java b/core/java/android/widget/Scroller.java index 3bfd39d..1a0ce9c 100644 --- a/core/java/android/widget/Scroller.java +++ b/core/java/android/widget/Scroller.java @@ -61,6 +61,8 @@ import android.view.animation.Interpolator; * }</pre> */ public class Scroller { + private final Interpolator mInterpolator; + private int mMode; private int mStartX; @@ -81,7 +83,6 @@ public class Scroller { private float mDeltaX; private float mDeltaY; private boolean mFinished; - private Interpolator mInterpolator; private boolean mFlywheel; private float mVelocity; @@ -142,18 +143,8 @@ public class Scroller { SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y; } SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f; - - // This controls the viscous fluid effect (how much of it) - sViscousFluidScale = 8.0f; - // must be set to 1.0 (used in viscousFluid()) - sViscousFluidNormalize = 1.0f; - sViscousFluidNormalize = 1.0f / viscousFluid(1.0f); - } - private static float sViscousFluidScale; - private static float sViscousFluidNormalize; - /** * Create a Scroller with the default duration and interpolator. */ @@ -178,7 +169,11 @@ public class Scroller { */ public Scroller(Context context, Interpolator interpolator, boolean flywheel) { mFinished = true; - mInterpolator = interpolator; + if (interpolator == null) { + mInterpolator = new ViscousFluidInterpolator(); + } else { + mInterpolator = interpolator; + } mPpi = context.getResources().getDisplayMetrics().density * 160.0f; mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction()); mFlywheel = flywheel; @@ -312,13 +307,7 @@ public class Scroller { if (timePassed < mDuration) { switch (mMode) { case SCROLL_MODE: - float x = timePassed * mDurationReciprocal; - - if (mInterpolator == null) - x = viscousFluid(x); - else - x = mInterpolator.getInterpolation(x); - + final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal); mCurrX = mStartX + Math.round(x * mDeltaX); mCurrY = mStartY + Math.round(x * mDeltaY); break; @@ -499,20 +488,6 @@ public class Scroller { return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l); } - static float viscousFluid(float x) - { - x *= sViscousFluidScale; - if (x < 1.0f) { - x -= (1.0f - (float)Math.exp(-x)); - } else { - float start = 0.36787944117f; // 1/e == exp(-1) - x = 1.0f - (float)Math.exp(1.0f - x); - x = start + x * (1.0f - start); - } - x *= sViscousFluidNormalize; - return x; - } - /** * Stops the animation. Contrary to {@link #forceFinished(boolean)}, * aborting the animating cause the scroller to move to the final x and y @@ -583,4 +558,41 @@ public class Scroller { return !mFinished && Math.signum(xvel) == Math.signum(mFinalX - mStartX) && Math.signum(yvel) == Math.signum(mFinalY - mStartY); } + + static class ViscousFluidInterpolator implements Interpolator { + /** Controls the viscous fluid effect (how much of it). */ + private static final float VISCOUS_FLUID_SCALE = 8.0f; + + private static final float VISCOUS_FLUID_NORMALIZE; + private static final float VISCOUS_FLUID_OFFSET; + + static { + + // must be set to 1.0 (used in viscousFluid()) + VISCOUS_FLUID_NORMALIZE = 1.0f / viscousFluid(1.0f); + // account for very small floating-point error + VISCOUS_FLUID_OFFSET = 1.0f - VISCOUS_FLUID_NORMALIZE * viscousFluid(1.0f); + } + + private static float viscousFluid(float x) { + x *= VISCOUS_FLUID_SCALE; + if (x < 1.0f) { + x -= (1.0f - (float)Math.exp(-x)); + } else { + float start = 0.36787944117f; // 1/e == exp(-1) + x = 1.0f - (float)Math.exp(1.0f - x); + x = start + x * (1.0f - start); + } + return x; + } + + @Override + public float getInterpolation(float input) { + final float interpolated = VISCOUS_FLUID_NORMALIZE * viscousFluid(input); + if (input > 0) { + return input + VISCOUS_FLUID_OFFSET; + } + return input; + } + } } diff --git a/core/java/android/widget/SearchView.java b/core/java/android/widget/SearchView.java index 0281602..d8a6867 100644 --- a/core/java/android/widget/SearchView.java +++ b/core/java/android/widget/SearchView.java @@ -242,7 +242,15 @@ public class SearchView extends LinearLayout implements CollapsibleActionView { } public SearchView(Context context, AttributeSet attrs) { - super(context, attrs); + this(context, attrs, 0); + } + + public SearchView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public SearchView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); LayoutInflater inflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); @@ -281,7 +289,8 @@ public class SearchView extends LinearLayout implements CollapsibleActionView { } }); - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SearchView, 0, 0); + TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.SearchView, defStyleAttr, defStyleRes); setIconifiedByDefault(a.getBoolean(R.styleable.SearchView_iconifiedByDefault, true)); int maxWidth = a.getDimensionPixelSize(R.styleable.SearchView_maxWidth, -1); if (maxWidth != -1) { @@ -304,7 +313,7 @@ public class SearchView extends LinearLayout implements CollapsibleActionView { boolean focusable = true; - a = context.obtainStyledAttributes(attrs, R.styleable.View, 0, 0); + a = context.obtainStyledAttributes(attrs, R.styleable.View, defStyleAttr, defStyleRes); focusable = a.getBoolean(R.styleable.View_focusable, focusable); a.recycle(); setFocusable(focusable); @@ -1050,7 +1059,7 @@ public class SearchView extends LinearLayout implements CollapsibleActionView { SpannableStringBuilder ssb = new SpannableStringBuilder(" "); // for the icon ssb.append(hintText); - Drawable searchIcon = getContext().getResources().getDrawable(getSearchIconId()); + Drawable searchIcon = getContext().getDrawable(getSearchIconId()); int textSize = (int) (mQueryTextView.getTextSize() * 1.25); searchIcon.setBounds(0, 0, textSize, textSize); ssb.setSpan(new ImageSpan(searchIcon), 1, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); @@ -1162,8 +1171,8 @@ public class SearchView extends LinearLayout implements CollapsibleActionView { || !mOnQueryChangeListener.onQueryTextSubmit(query.toString())) { if (mSearchable != null) { launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, query.toString()); - setImeVisibility(false); } + setImeVisibility(false); dismissSuggestions(); } } @@ -1661,8 +1670,14 @@ public class SearchView extends LinearLayout implements CollapsibleActionView { mThreshold = getThreshold(); } - public SearchAutoComplete(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public SearchAutoComplete(Context context, AttributeSet attrs, int defStyleAttrs) { + super(context, attrs, defStyleAttrs); + mThreshold = getThreshold(); + } + + public SearchAutoComplete( + Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) { + super(context, attrs, defStyleAttrs, defStyleRes); mThreshold = getThreshold(); } diff --git a/core/java/android/widget/SeekBar.java b/core/java/android/widget/SeekBar.java index 2737f94..dc7c04c 100644 --- a/core/java/android/widget/SeekBar.java +++ b/core/java/android/widget/SeekBar.java @@ -79,8 +79,12 @@ public class SeekBar extends AbsSeekBar { this(context, attrs, com.android.internal.R.attr.seekBarStyle); } - public SeekBar(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public SeekBar(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public SeekBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); } @Override diff --git a/core/java/android/widget/ShareActionProvider.java b/core/java/android/widget/ShareActionProvider.java index fd6ca4c..cde8080 100644 --- a/core/java/android/widget/ShareActionProvider.java +++ b/core/java/android/widget/ShareActionProvider.java @@ -170,7 +170,7 @@ public class ShareActionProvider extends ActionProvider { // Lookup and set the expand action icon. TypedValue outTypedValue = new TypedValue(); mContext.getTheme().resolveAttribute(R.attr.actionModeShareDrawable, outTypedValue, true); - Drawable drawable = mContext.getResources().getDrawable(outTypedValue.resourceId); + Drawable drawable = mContext.getDrawable(outTypedValue.resourceId); activityChooserView.setExpandActivityOverflowButtonDrawable(drawable); activityChooserView.setProvider(this); diff --git a/core/java/android/widget/SlidingDrawer.java b/core/java/android/widget/SlidingDrawer.java index 517246b..ec06c02 100644 --- a/core/java/android/widget/SlidingDrawer.java +++ b/core/java/android/widget/SlidingDrawer.java @@ -192,11 +192,32 @@ public class SlidingDrawer extends ViewGroup { * * @param context The application's environment. * @param attrs The attributes defined in XML. - * @param defStyle The style to apply to this widget. + * @param defStyleAttr An attribute in the current theme that contains a + * reference to a style resource that supplies default values for + * the view. Can be 0 to not look for defaults. */ - public SlidingDrawer(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SlidingDrawer, defStyle, 0); + public SlidingDrawer(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + /** + * Creates a new SlidingDrawer from a specified set of attributes defined in XML. + * + * @param context The application's environment. + * @param attrs The attributes defined in XML. + * @param defStyleAttr An attribute in the current theme that contains a + * reference to a style resource that supplies default values for + * the view. Can be 0 to not look for defaults. + * @param defStyleRes A resource identifier of a style resource that + * supplies default values for the view, used only if + * defStyleAttr is 0 or can not be found in the theme. Can be 0 + * to not look for defaults. + */ + public SlidingDrawer(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + final TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.SlidingDrawer, defStyleAttr, defStyleRes); int orientation = a.getInt(R.styleable.SlidingDrawer_orientation, ORIENTATION_VERTICAL); mVertical = orientation == ORIENTATION_VERTICAL; diff --git a/core/java/android/widget/Space.java b/core/java/android/widget/Space.java index bb53a77..c4eaeb7 100644 --- a/core/java/android/widget/Space.java +++ b/core/java/android/widget/Space.java @@ -20,7 +20,6 @@ import android.content.Context; import android.graphics.Canvas; import android.util.AttributeSet; import android.view.View; -import android.view.ViewGroup; /** * Space is a lightweight View subclass that may be used to create gaps between components @@ -30,8 +29,8 @@ public final class Space extends View { /** * {@inheritDoc} */ - public Space(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public Space(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); if (getVisibility() == VISIBLE) { setVisibility(INVISIBLE); } @@ -40,6 +39,13 @@ public final class Space extends View { /** * {@inheritDoc} */ + public Space(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + /** + * {@inheritDoc} + */ public Space(Context context, AttributeSet attrs) { this(context, attrs, 0); } diff --git a/core/java/android/widget/SpellChecker.java b/core/java/android/widget/SpellChecker.java index b204dfd..595f023 100644 --- a/core/java/android/widget/SpellChecker.java +++ b/core/java/android/widget/SpellChecker.java @@ -35,6 +35,7 @@ import android.view.textservice.TextInfo; import android.view.textservice.TextServicesManager; import com.android.internal.util.ArrayUtils; +import com.android.internal.util.GrowingArrayUtils; import java.text.BreakIterator; import java.util.Locale; @@ -105,9 +106,9 @@ public class SpellChecker implements SpellCheckerSessionListener { mTextView = textView; // Arbitrary: these arrays will automatically double their sizes on demand - final int size = ArrayUtils.idealObjectArraySize(1); - mIds = new int[size]; - mSpellCheckSpans = new SpellCheckSpan[size]; + final int size = 1; + mIds = ArrayUtils.newUnpaddedIntArray(size); + mSpellCheckSpans = new SpellCheckSpan[mIds.length]; setLocale(mTextView.getSpellCheckerLocale()); @@ -184,17 +185,9 @@ public class SpellChecker implements SpellCheckerSessionListener { if (mIds[i] < 0) return i; } - if (mLength == mSpellCheckSpans.length) { - final int newSize = mLength * 2; - int[] newIds = new int[newSize]; - SpellCheckSpan[] newSpellCheckSpans = new SpellCheckSpan[newSize]; - System.arraycopy(mIds, 0, newIds, 0, mLength); - System.arraycopy(mSpellCheckSpans, 0, newSpellCheckSpans, 0, mLength); - mIds = newIds; - mSpellCheckSpans = newSpellCheckSpans; - } - - mSpellCheckSpans[mLength] = new SpellCheckSpan(); + mIds = GrowingArrayUtils.append(mIds, mLength, 0); + mSpellCheckSpans = GrowingArrayUtils.append( + mSpellCheckSpans, mLength, new SpellCheckSpan()); mLength++; return mLength - 1; } @@ -731,10 +724,14 @@ public class SpellChecker implements SpellCheckerSessionListener { } } - if (scheduleOtherSpellCheck) { + if (scheduleOtherSpellCheck && wordStart <= end) { // Update range span: start new spell check from last wordStart setRangeSpan(editable, wordStart, end); } else { + if (DBG && scheduleOtherSpellCheck) { + Log.w(TAG, "Trying to schedule spellcheck for invalid region, from " + + wordStart + " to " + end); + } removeRangeSpan(editable); } diff --git a/core/java/android/widget/Spinner.java b/core/java/android/widget/Spinner.java index 5cbabef..9601d4a 100644 --- a/core/java/android/widget/Spinner.java +++ b/core/java/android/widget/Spinner.java @@ -130,18 +130,17 @@ public class Spinner extends AbsSpinner implements OnClickListener { /** * Construct a new spinner with the given context's theme, the supplied attribute set, - * and default style. + * and default style attribute. * * @param context The Context the view is running in, through which it can * access the current theme, resources, etc. * @param attrs The attributes of the XML tag that is inflating the view. - * @param defStyle The default style to apply to this view. If 0, no style - * will be applied (beyond what is included in the theme). This may - * either be an attribute resource, whose value will be retrieved - * from the current theme, or an explicit style resource. + * @param defStyleAttr An attribute in the current theme that contains a + * reference to a style resource that supplies default values for + * the view. Can be 0 to not look for defaults. */ - public Spinner(Context context, AttributeSet attrs, int defStyle) { - this(context, attrs, defStyle, MODE_THEME); + public Spinner(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0, MODE_THEME); } /** @@ -152,20 +151,44 @@ public class Spinner extends AbsSpinner implements OnClickListener { * @param context The Context the view is running in, through which it can * access the current theme, resources, etc. * @param attrs The attributes of the XML tag that is inflating the view. - * @param defStyle The default style to apply to this view. If 0, no style - * will be applied (beyond what is included in the theme). This may - * either be an attribute resource, whose value will be retrieved - * from the current theme, or an explicit style resource. + * @param defStyleAttr An attribute in the current theme that contains a + * reference to a style resource that supplies default values for + * the view. Can be 0 to not look for defaults. * @param mode Constant describing how the user will select choices from the spinner. - * + * + * @see #MODE_DIALOG + * @see #MODE_DROPDOWN + */ + public Spinner(Context context, AttributeSet attrs, int defStyleAttr, int mode) { + this(context, attrs, defStyleAttr, 0, mode); + } + + /** + * Construct a new spinner with the given context's theme, the supplied attribute set, + * and default style. <code>mode</code> may be one of {@link #MODE_DIALOG} or + * {@link #MODE_DROPDOWN} and determines how the user will select choices from the spinner. + * + * @param context The Context the view is running in, through which it can + * access the current theme, resources, etc. + * @param attrs The attributes of the XML tag that is inflating the view. + * @param defStyleAttr An attribute in the current theme that contains a + * reference to a style resource that supplies default values for + * the view. Can be 0 to not look for defaults. + * @param defStyleRes A resource identifier of a style resource that + * supplies default values for the view, used only if + * defStyleAttr is 0 or can not be found in the theme. Can be 0 + * to not look for defaults. + * @param mode Constant describing how the user will select choices from the spinner. + * * @see #MODE_DIALOG * @see #MODE_DROPDOWN */ - public Spinner(Context context, AttributeSet attrs, int defStyle, int mode) { - super(context, attrs, defStyle); + public Spinner( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes, int mode) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.Spinner, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.Spinner, defStyleAttr, defStyleRes); if (mode == MODE_THEME) { mode = a.getInt(com.android.internal.R.styleable.Spinner_spinnerMode, MODE_DIALOG); @@ -178,7 +201,7 @@ public class Spinner extends AbsSpinner implements OnClickListener { } case MODE_DROPDOWN: { - final DropdownPopup popup = new DropdownPopup(context, attrs, defStyle); + final DropdownPopup popup = new DropdownPopup(context, attrs, defStyleAttr, defStyleRes); mDropDownWidth = a.getLayoutDimension( com.android.internal.R.styleable.Spinner_dropDownWidth, @@ -258,7 +281,7 @@ public class Spinner extends AbsSpinner implements OnClickListener { * @attr ref android.R.styleable#Spinner_popupBackground */ public void setPopupBackgroundResource(int resId) { - setPopupBackgroundDrawable(getContext().getResources().getDrawable(resId)); + setPopupBackgroundDrawable(getContext().getDrawable(resId)); } /** @@ -1033,8 +1056,9 @@ public class Spinner extends AbsSpinner implements OnClickListener { private CharSequence mHintText; private ListAdapter mAdapter; - public DropdownPopup(Context context, AttributeSet attrs, int defStyleRes) { - super(context, attrs, 0, defStyleRes); + public DropdownPopup( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); setAnchorView(Spinner.this); setModal(true); diff --git a/core/java/android/widget/StackView.java b/core/java/android/widget/StackView.java index 6853660..d2e718c 100644 --- a/core/java/android/widget/StackView.java +++ b/core/java/android/widget/StackView.java @@ -168,9 +168,16 @@ public class StackView extends AdapterViewAnimator { * {@inheritDoc} */ public StackView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.StackView, defStyleAttr, 0); + this(context, attrs, defStyleAttr, 0); + } + + /** + * {@inheritDoc} + */ + public StackView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.StackView, defStyleAttr, defStyleRes); mResOutColor = a.getColor( com.android.internal.R.styleable.StackView_resOutColor, 0); diff --git a/core/java/android/widget/SuggestionsAdapter.java b/core/java/android/widget/SuggestionsAdapter.java index c44d431..0203301 100644 --- a/core/java/android/widget/SuggestionsAdapter.java +++ b/core/java/android/widget/SuggestionsAdapter.java @@ -529,7 +529,7 @@ class SuggestionsAdapter extends ResourceCursorAdapter implements OnClickListene return drawable; } // Not cached, find it by resource ID - drawable = mProviderContext.getResources().getDrawable(resourceId); + drawable = mProviderContext.getDrawable(resourceId); // Stick it in the cache, using the URI as key storeInIconCache(drawableUri, drawable); return drawable; @@ -563,7 +563,7 @@ class SuggestionsAdapter extends ResourceCursorAdapter implements OnClickListene OpenResourceIdResult r = mProviderContext.getContentResolver().getResourceId(uri); try { - return r.r.getDrawable(r.id); + return r.r.getDrawable(r.id, mContext.getTheme()); } catch (Resources.NotFoundException ex) { throw new FileNotFoundException("Resource does not exist: " + uri); } @@ -574,7 +574,7 @@ class SuggestionsAdapter extends ResourceCursorAdapter implements OnClickListene throw new FileNotFoundException("Failed to open " + uri); } try { - return Drawable.createFromStream(stream, null); + return Drawable.createFromStreamThemed(stream, null, mContext.getTheme()); } finally { try { stream.close(); diff --git a/core/java/android/widget/Switch.java b/core/java/android/widget/Switch.java index e754c17..3d23e4d 100644 --- a/core/java/android/widget/Switch.java +++ b/core/java/android/widget/Switch.java @@ -16,6 +16,7 @@ package android.widget; +import android.animation.ObjectAnimator; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Resources; @@ -32,6 +33,8 @@ import android.text.TextUtils; import android.text.method.AllCapsTransformationMethod; import android.text.method.TransformationMethod2; import android.util.AttributeSet; +import android.util.FloatProperty; +import android.util.MathUtils; import android.view.Gravity; import android.view.MotionEvent; import android.view.VelocityTracker; @@ -66,6 +69,8 @@ import com.android.internal.R; * @attr ref android.R.styleable#Switch_track */ public class Switch extends CompoundButton { + private static final int THUMB_ANIMATION_DURATION = 250; + private static final int TOUCH_MODE_IDLE = 0; private static final int TOUCH_MODE_DOWN = 1; private static final int TOUCH_MODE_DRAGGING = 2; @@ -105,6 +110,7 @@ public class Switch extends CompoundButton { private Layout mOnLayout; private Layout mOffLayout; private TransformationMethod2 mSwitchTransformationMethod; + private ObjectAnimator mPositionAnimator; @SuppressWarnings("hiding") private final Rect mTempRect = new Rect(); @@ -139,19 +145,41 @@ public class Switch extends CompoundButton { * * @param context The Context that will determine this widget's theming. * @param attrs Specification of attributes that should deviate from the default styling. - * @param defStyle An attribute ID within the active theme containing a reference to the - * default style for this widget. e.g. android.R.attr.switchStyle. + * @param defStyleAttr An attribute in the current theme that contains a + * reference to a style resource that supplies default values for + * the view. Can be 0 to not look for defaults. + */ + public Switch(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + + /** + * Construct a new Switch with a default style determined by the given theme + * attribute or style resource, overriding specific style attributes as + * requested. + * + * @param context The Context that will determine this widget's theming. + * @param attrs Specification of attributes that should deviate from the + * default styling. + * @param defStyleAttr An attribute in the current theme that contains a + * reference to a style resource that supplies default values for + * the view. Can be 0 to not look for defaults. + * @param defStyleRes A resource identifier of a style resource that + * supplies default values for the view, used only if + * defStyleAttr is 0 or can not be found in the theme. Can be 0 + * to not look for defaults. */ - public Switch(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public Switch(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); Resources res = getResources(); mTextPaint.density = res.getDisplayMetrics().density; mTextPaint.setCompatibilityScaling(res.getCompatibilityInfo().applicationScale); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.Switch, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.Switch, defStyleAttr, defStyleRes); mThumbDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_thumb); mTrackDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_track); @@ -389,7 +417,7 @@ public class Switch extends CompoundButton { * @attr ref android.R.styleable#Switch_track */ public void setTrackResource(int resId) { - setTrackDrawable(getContext().getResources().getDrawable(resId)); + setTrackDrawable(getContext().getDrawable(resId)); } /** @@ -425,7 +453,7 @@ public class Switch extends CompoundButton { * @attr ref android.R.styleable#Switch_thumb */ public void setThumbResource(int resId) { - setThumbDrawable(getContext().getResources().getDrawable(resId)); + setThumbDrawable(getContext().getDrawable(resId)); } /** @@ -483,15 +511,18 @@ public class Switch extends CompoundButton { if (mOnLayout == null) { mOnLayout = makeLayout(mTextOn); } + if (mOffLayout == null) { mOffLayout = makeLayout(mTextOff); } mTrackDrawable.getPadding(mTempRect); + final int maxTextWidth = Math.max(mOnLayout.getWidth(), mOffLayout.getWidth()); final int switchWidth = Math.max(mSwitchMinWidth, maxTextWidth * 2 + mThumbTextPadding * 4 + mTempRect.left + mTempRect.right); - final int switchHeight = mTrackDrawable.getIntrinsicHeight(); + final int switchHeight = Math.max(mTrackDrawable.getIntrinsicHeight(), + mThumbDrawable.getIntrinsicHeight()); mThumbWidth = maxTextWidth + mThumbTextPadding * 2; @@ -528,9 +559,12 @@ public class Switch extends CompoundButton { * @return true if (x, y) is within the target area of the switch thumb */ private boolean hitThumb(float x, float y) { + // Relies on mTempRect, MUST be called first! + final int thumbOffset = getThumbOffset(); + mThumbDrawable.getPadding(mTempRect); final int thumbTop = mSwitchTop - mTouchSlop; - final int thumbLeft = mSwitchLeft + (int) (mThumbPosition + 0.5f) - mTouchSlop; + final int thumbLeft = mSwitchLeft + thumbOffset - mTouchSlop; final int thumbRight = thumbLeft + mThumbWidth + mTempRect.left + mTempRect.right + mTouchSlop; final int thumbBottom = mSwitchBottom + mTouchSlop; @@ -575,13 +609,23 @@ public class Switch extends CompoundButton { case TOUCH_MODE_DRAGGING: { final float x = ev.getX(); - final float dx = x - mTouchX; - float newPos = Math.max(0, - Math.min(mThumbPosition + dx, getThumbScrollRange())); + final int thumbScrollRange = getThumbScrollRange(); + final float thumbScrollOffset = x - mTouchX; + float dPos; + if (thumbScrollRange != 0) { + dPos = thumbScrollOffset / thumbScrollRange; + } else { + // If the thumb scroll range is empty, just use the + // movement direction to snap on or off. + dPos = thumbScrollOffset > 0 ? 1 : -1; + } + if (isLayoutRtl()) { + dPos = -dPos; + } + final float newPos = MathUtils.constrain(mThumbPosition + dPos, 0, 1); if (newPos != mThumbPosition) { - mThumbPosition = newPos; mTouchX = x; - invalidate(); + setThumbPosition(newPos); } return true; } @@ -618,62 +662,77 @@ public class Switch extends CompoundButton { */ private void stopDrag(MotionEvent ev) { mTouchMode = TOUCH_MODE_IDLE; - // Up and not canceled, also checks the switch has not been disabled during the drag - boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled(); - - cancelSuperTouch(ev); + // Commit the change if the event is up and not canceled and the switch + // has not been disabled during the drag. + final boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled(); + final boolean newState; if (commitChange) { - boolean newState; mVelocityTracker.computeCurrentVelocity(1000); - float xvel = mVelocityTracker.getXVelocity(); + final float xvel = mVelocityTracker.getXVelocity(); if (Math.abs(xvel) > mMinFlingVelocity) { newState = isLayoutRtl() ? (xvel < 0) : (xvel > 0); } else { newState = getTargetCheckedState(); } - animateThumbToCheckedState(newState); } else { - animateThumbToCheckedState(isChecked()); + newState = isChecked(); } + + setChecked(newState); + cancelSuperTouch(ev); } private void animateThumbToCheckedState(boolean newCheckedState) { - // TODO animate! - //float targetPos = newCheckedState ? 0 : getThumbScrollRange(); - //mThumbPosition = targetPos; - setChecked(newCheckedState); + final float targetPosition = newCheckedState ? 1 : 0; + mPositionAnimator = ObjectAnimator.ofFloat(this, THUMB_POS, targetPosition); + mPositionAnimator.setDuration(THUMB_ANIMATION_DURATION); + mPositionAnimator.setAutoCancel(true); + mPositionAnimator.start(); } - private boolean getTargetCheckedState() { - if (isLayoutRtl()) { - return mThumbPosition <= getThumbScrollRange() / 2; - } else { - return mThumbPosition >= getThumbScrollRange() / 2; + private void cancelPositionAnimator() { + if (mPositionAnimator != null) { + mPositionAnimator.cancel(); } } - private void setThumbPosition(boolean checked) { - if (isLayoutRtl()) { - mThumbPosition = checked ? 0 : getThumbScrollRange(); - } else { - mThumbPosition = checked ? getThumbScrollRange() : 0; - } + private boolean getTargetCheckedState() { + return mThumbPosition > 0.5f; + } + + /** + * Sets the thumb position as a decimal value between 0 (off) and 1 (on). + * + * @param position new position between [0,1] + */ + private void setThumbPosition(float position) { + mThumbPosition = position; + invalidate(); + } + + @Override + public void toggle() { + setChecked(!isChecked()); } @Override public void setChecked(boolean checked) { super.setChecked(checked); - setThumbPosition(isChecked()); - invalidate(); + + if (isAttachedToWindow() && isLaidOut()) { + animateThumbToCheckedState(checked); + } else { + // Immediately move the thumb to the new position. + cancelPositionAnimator(); + setThumbPosition(checked ? 1 : 0); + } } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); - setThumbPosition(isChecked()); - int switchRight; int switchLeft; @@ -716,47 +775,51 @@ public class Switch extends CompoundButton { protected void onDraw(Canvas canvas) { super.onDraw(canvas); - // Draw the switch - int switchLeft = mSwitchLeft; - int switchTop = mSwitchTop; - int switchRight = mSwitchRight; - int switchBottom = mSwitchBottom; - - mTrackDrawable.setBounds(switchLeft, switchTop, switchRight, switchBottom); - mTrackDrawable.draw(canvas); + final Rect tempRect = mTempRect; + final Drawable trackDrawable = mTrackDrawable; + final Drawable thumbDrawable = mThumbDrawable; - canvas.save(); - - mTrackDrawable.getPadding(mTempRect); - int switchInnerLeft = switchLeft + mTempRect.left; - int switchInnerTop = switchTop + mTempRect.top; - int switchInnerRight = switchRight - mTempRect.right; - int switchInnerBottom = switchBottom - mTempRect.bottom; + // Draw the switch + final int switchLeft = mSwitchLeft; + final int switchTop = mSwitchTop; + final int switchRight = mSwitchRight; + final int switchBottom = mSwitchBottom; + trackDrawable.setBounds(switchLeft, switchTop, switchRight, switchBottom); + trackDrawable.draw(canvas); + + final int saveCount = canvas.save(); + + trackDrawable.getPadding(tempRect); + final int switchInnerLeft = switchLeft + tempRect.left; + final int switchInnerTop = switchTop + tempRect.top; + final int switchInnerRight = switchRight - tempRect.right; + final int switchInnerBottom = switchBottom - tempRect.bottom; canvas.clipRect(switchInnerLeft, switchTop, switchInnerRight, switchBottom); - mThumbDrawable.getPadding(mTempRect); - final int thumbPos = (int) (mThumbPosition + 0.5f); - int thumbLeft = switchInnerLeft - mTempRect.left + thumbPos; - int thumbRight = switchInnerLeft + thumbPos + mThumbWidth + mTempRect.right; + // Relies on mTempRect, MUST be called first! + final int thumbPos = getThumbOffset(); - mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom); - mThumbDrawable.draw(canvas); + thumbDrawable.getPadding(tempRect); + int thumbLeft = switchInnerLeft - tempRect.left + thumbPos; + int thumbRight = switchInnerLeft + thumbPos + mThumbWidth + tempRect.right; + thumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom); + thumbDrawable.draw(canvas); - // mTextColors should not be null, but just in case + final int drawableState[] = getDrawableState(); if (mTextColors != null) { - mTextPaint.setColor(mTextColors.getColorForState(getDrawableState(), - mTextColors.getDefaultColor())); + mTextPaint.setColor(mTextColors.getColorForState(drawableState, 0)); } - mTextPaint.drawableState = getDrawableState(); + mTextPaint.drawableState = drawableState; - Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout; + final Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout; if (switchText != null) { - canvas.translate((thumbLeft + thumbRight) / 2 - switchText.getWidth() / 2, - (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2); + final int left = (thumbLeft + thumbRight) / 2 - switchText.getWidth() / 2; + final int top = (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2; + canvas.translate(left, top); switchText.draw(canvas); } - canvas.restore(); + canvas.restoreToCount(saveCount); } @Override @@ -783,6 +846,22 @@ public class Switch extends CompoundButton { return padding; } + /** + * Translates thumb position to offset according to current RTL setting and + * thumb scroll range. + * + * @return thumb offset + */ + private int getThumbOffset() { + final float thumbPosition; + if (isLayoutRtl()) { + thumbPosition = 1 - mThumbPosition; + } else { + thumbPosition = mThumbPosition; + } + return (int) (thumbPosition * getThumbScrollRange() + 0.5f); + } + private int getThumbScrollRange() { if (mTrackDrawable == null) { return 0; @@ -848,4 +927,16 @@ public class Switch extends CompoundButton { } } } + + private static final FloatProperty<Switch> THUMB_POS = new FloatProperty<Switch>("thumbPos") { + @Override + public Float get(Switch object) { + return object.mThumbPosition; + } + + @Override + public void setValue(Switch object, float value) { + object.setThumbPosition(value); + } + }; } diff --git a/core/java/android/widget/TabHost.java b/core/java/android/widget/TabHost.java index 238dc55..89df51a 100644 --- a/core/java/android/widget/TabHost.java +++ b/core/java/android/widget/TabHost.java @@ -77,11 +77,18 @@ public class TabHost extends FrameLayout implements ViewTreeObserver.OnTouchMode } public TabHost(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.tabWidgetStyle); + } + + public TabHost(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public TabHost(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.TabWidget, - com.android.internal.R.attr.tabWidgetStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.TabWidget, defStyleAttr, defStyleRes); mTabLayoutId = a.getResourceId(R.styleable.TabWidget_tabLayout, 0); a.recycle(); diff --git a/core/java/android/widget/TabWidget.java b/core/java/android/widget/TabWidget.java index 6bced1c..47a5449 100644 --- a/core/java/android/widget/TabWidget.java +++ b/core/java/android/widget/TabWidget.java @@ -74,11 +74,15 @@ public class TabWidget extends LinearLayout implements OnFocusChangeListener { this(context, attrs, com.android.internal.R.attr.tabWidgetStyle); } - public TabWidget(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public TabWidget(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public TabWidget(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); final TypedArray a = context.obtainStyledAttributes( - attrs, com.android.internal.R.styleable.TabWidget, defStyle, 0); + attrs, com.android.internal.R.styleable.TabWidget, defStyleAttr, defStyleRes); setStripEnabled(a.getBoolean(R.styleable.TabWidget_tabStripEnabled, true)); setLeftStripDrawable(a.getDrawable(R.styleable.TabWidget_tabStripLeft)); @@ -116,28 +120,27 @@ public class TabWidget extends LinearLayout implements OnFocusChangeListener { setChildrenDrawingOrderEnabled(true); final Context context = mContext; - final Resources resources = context.getResources(); // Tests the target Sdk version, as set in the Manifest. Could not be set using styles.xml // in a values-v? directory which targets the current platform Sdk version instead. if (context.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.DONUT) { // Donut apps get old color scheme if (mLeftStrip == null) { - mLeftStrip = resources.getDrawable( + mLeftStrip = context.getDrawable( com.android.internal.R.drawable.tab_bottom_left_v4); } if (mRightStrip == null) { - mRightStrip = resources.getDrawable( + mRightStrip = context.getDrawable( com.android.internal.R.drawable.tab_bottom_right_v4); } } else { // Use modern color scheme for Eclair and beyond if (mLeftStrip == null) { - mLeftStrip = resources.getDrawable( + mLeftStrip = context.getDrawable( com.android.internal.R.drawable.tab_bottom_left); } if (mRightStrip == null) { - mRightStrip = resources.getDrawable( + mRightStrip = context.getDrawable( com.android.internal.R.drawable.tab_bottom_right); } } @@ -242,7 +245,7 @@ public class TabWidget extends LinearLayout implements OnFocusChangeListener { * divider. */ public void setDividerDrawable(int resId) { - setDividerDrawable(getResources().getDrawable(resId)); + setDividerDrawable(mContext.getDrawable(resId)); } /** @@ -263,7 +266,7 @@ public class TabWidget extends LinearLayout implements OnFocusChangeListener { * left strip drawable */ public void setLeftStripDrawable(int resId) { - setLeftStripDrawable(getResources().getDrawable(resId)); + setLeftStripDrawable(mContext.getDrawable(resId)); } /** @@ -284,7 +287,7 @@ public class TabWidget extends LinearLayout implements OnFocusChangeListener { * right strip drawable */ public void setRightStripDrawable(int resId) { - setRightStripDrawable(getResources().getDrawable(resId)); + setRightStripDrawable(mContext.getDrawable(resId)); } /** diff --git a/core/java/android/widget/TextClock.java b/core/java/android/widget/TextClock.java index b3b95d9..4c5c71d 100644 --- a/core/java/android/widget/TextClock.java +++ b/core/java/android/widget/TextClock.java @@ -198,15 +198,19 @@ public class TextClock extends TextView { * @param context The Context the view is running in, through which it can * access the current theme, resources, etc. * @param attrs The attributes of the XML tag that is inflating the view - * @param defStyle The default style to apply to this view. If 0, no style - * will be applied (beyond what is included in the theme). This may - * either be an attribute resource, whose value will be retrieved - * from the current theme, or an explicit style resource + * @param defStyleAttr An attribute in the current theme that contains a + * reference to a style resource that supplies default values for + * the view. Can be 0 to not look for defaults. */ - public TextClock(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public TextClock(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public TextClock(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TextClock, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.TextClock, defStyleAttr, defStyleRes); try { mFormat12 = a.getText(R.styleable.TextClock_format12Hour); mFormat24 = a.getText(R.styleable.TextClock_format24Hour); diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 8460375..a7278da 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -136,7 +136,6 @@ import java.io.IOException; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Locale; -import java.util.concurrent.locks.ReentrantLock; import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; @@ -618,9 +617,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener this(context, attrs, com.android.internal.R.attr.textViewStyle); } + public TextView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + @SuppressWarnings("deprecation") - public TextView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public TextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + mText = ""; final Resources res = getResources(); @@ -657,8 +661,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * to be able to parse the appearance first and then let specific tags * for this View override it. */ - TypedArray a = theme.obtainStyledAttributes( - attrs, com.android.internal.R.styleable.TextViewAppearance, defStyle, 0); + TypedArray a = theme.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.TextViewAppearance, defStyleAttr, defStyleRes); TypedArray appearance = null; int ap = a.getResourceId( com.android.internal.R.styleable.TextViewAppearance_textAppearance, -1); @@ -751,7 +755,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int inputType = EditorInfo.TYPE_NULL; a = theme.obtainStyledAttributes( - attrs, com.android.internal.R.styleable.TextView, defStyle, 0); + attrs, com.android.internal.R.styleable.TextView, defStyleAttr, defStyleRes); int n = a.getIndexCount(); for (int i = 0; i < n; i++) { @@ -1275,9 +1279,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * However, TextViews that have input or movement methods *are* * focusable by default. */ - a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.View, - defStyle, 0); + a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes); boolean focusable = mMovement != null || getKeyListener() != null; boolean clickable = focusable; @@ -2070,11 +2073,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ @android.view.RemotableViewMethod public void setCompoundDrawablesWithIntrinsicBounds(int left, int top, int right, int bottom) { - final Resources resources = getContext().getResources(); - setCompoundDrawablesWithIntrinsicBounds(left != 0 ? resources.getDrawable(left) : null, - top != 0 ? resources.getDrawable(top) : null, - right != 0 ? resources.getDrawable(right) : null, - bottom != 0 ? resources.getDrawable(bottom) : null); + final Context context = getContext(); + setCompoundDrawablesWithIntrinsicBounds(left != 0 ? context.getDrawable(left) : null, + top != 0 ? context.getDrawable(top) : null, + right != 0 ? context.getDrawable(right) : null, + bottom != 0 ? context.getDrawable(bottom) : null); } /** @@ -2244,12 +2247,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @android.view.RemotableViewMethod public void setCompoundDrawablesRelativeWithIntrinsicBounds(int start, int top, int end, int bottom) { - final Resources resources = getContext().getResources(); + final Context context = getContext(); setCompoundDrawablesRelativeWithIntrinsicBounds( - start != 0 ? resources.getDrawable(start) : null, - top != 0 ? resources.getDrawable(top) : null, - end != 0 ? resources.getDrawable(end) : null, - bottom != 0 ? resources.getDrawable(bottom) : null); + start != 0 ? context.getDrawable(start) : null, + top != 0 ? context.getDrawable(top) : null, + end != 0 ? context.getDrawable(end) : null, + bottom != 0 ? context.getDrawable(bottom) : null); } /** @@ -4382,8 +4385,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (error == null) { setError(null, null); } else { - Drawable dr = getContext().getResources(). - getDrawable(com.android.internal.R.drawable.indicator_input_error); + Drawable dr = getContext().getDrawable( + com.android.internal.R.drawable.indicator_input_error); dr.setBounds(0, 0, dr.getIntrinsicWidth(), dr.getIntrinsicHeight()); setError(error, dr); @@ -4726,10 +4729,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (mEditor != null) mEditor.onAttachedToWindow(); } + /** @hide */ @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - + protected void onDetachedFromWindowInternal() { if (mPreDrawRegistered) { getViewTreeObserver().removeOnPreDrawListener(this); mPreDrawRegistered = false; @@ -4738,6 +4740,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener resetResolvedDrawables(); if (mEditor != null) mEditor.onDetachedFromWindow(); + + super.onDetachedFromWindowInternal(); } @Override @@ -4811,6 +4815,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @Override public void invalidateDrawable(Drawable drawable) { + boolean handled = false; + if (verifyDrawable(drawable)) { final Rect dirty = drawable.getBounds(); int scrollX = mScrollX; @@ -4828,6 +4834,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener scrollX += mPaddingLeft; scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightLeft) / 2; + handled = true; } else if (drawable == drawables.mDrawableRight) { final int compoundPaddingTop = getCompoundPaddingTop(); final int compoundPaddingBottom = getCompoundPaddingBottom(); @@ -4835,6 +4842,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener scrollX += (mRight - mLeft - mPaddingRight - drawables.mDrawableSizeRight); scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightRight) / 2; + handled = true; } else if (drawable == drawables.mDrawableTop) { final int compoundPaddingLeft = getCompoundPaddingLeft(); final int compoundPaddingRight = getCompoundPaddingRight(); @@ -4842,6 +4850,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener scrollX += compoundPaddingLeft + (hspace - drawables.mDrawableWidthTop) / 2; scrollY += mPaddingTop; + handled = true; } else if (drawable == drawables.mDrawableBottom) { final int compoundPaddingLeft = getCompoundPaddingLeft(); final int compoundPaddingRight = getCompoundPaddingRight(); @@ -4849,11 +4858,18 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener scrollX += compoundPaddingLeft + (hspace - drawables.mDrawableWidthBottom) / 2; scrollY += (mBottom - mTop - mPaddingBottom - drawables.mDrawableSizeBottom); + handled = true; } } - invalidate(dirty.left + scrollX, dirty.top + scrollY, - dirty.right + scrollX, dirty.bottom + scrollY); + if (handled) { + invalidate(dirty.left + scrollX, dirty.top + scrollY, + dirty.right + scrollX, dirty.bottom + scrollY); + } + } + + if (!handled) { + super.invalidateDrawable(drawable); } } @@ -5794,6 +5810,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int end = text.partialEndOffset; if (end > N) end = N; removeParcelableSpans(content, start, end); + // If start > end, content.replace will swap them before using them. content.replace(start, end, text.text); } } @@ -8478,7 +8495,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return false; } - if (mText.length() > 0 && hasSelection()) { + if (mText.length() > 0 && hasSelection() && mEditor != null) { return true; } diff --git a/core/java/android/widget/TimePicker.java b/core/java/android/widget/TimePicker.java index c26cb24..8e4ba0d 100644 --- a/core/java/android/widget/TimePicker.java +++ b/core/java/android/widget/TimePicker.java @@ -20,26 +20,17 @@ import android.annotation.Widget; import android.content.Context; import android.content.res.Configuration; import android.content.res.TypedArray; -import android.os.Parcel; import android.os.Parcelable; -import android.text.format.DateFormat; -import android.text.format.DateUtils; import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputMethodManager; -import android.widget.NumberPicker.OnValueChangeListener; import com.android.internal.R; -import java.text.DateFormatSymbols; -import java.util.Calendar; import java.util.Locale; +import static android.os.Build.VERSION_CODES.KITKAT; + /** * A view for selecting the time of day, in either 24 hour or AM/PM mode. The * hour, each minute digit, and AM/PM (if applicable) can be conrolled by @@ -57,58 +48,12 @@ import java.util.Locale; @Widget public class TimePicker extends FrameLayout { - private static final boolean DEFAULT_ENABLED_STATE = true; + private TimePickerDelegate mDelegate; - private static final int HOURS_IN_HALF_DAY = 12; - - /** - * A no-op callback used in the constructor to avoid null checks later in - * the code. - */ - private static final OnTimeChangedListener NO_OP_CHANGE_LISTENER = new OnTimeChangedListener() { - public void onTimeChanged(TimePicker view, int hourOfDay, int minute) { - } - }; - - // state - private boolean mIs24HourView; - - private boolean mIsAm; - - // ui components - private final NumberPicker mHourSpinner; - - private final NumberPicker mMinuteSpinner; - - private final NumberPicker mAmPmSpinner; - - private final EditText mHourSpinnerInput; - - private final EditText mMinuteSpinnerInput; - - private final EditText mAmPmSpinnerInput; - - private final TextView mDivider; - - // Note that the legacy implementation of the TimePicker is - // using a button for toggling between AM/PM while the new - // version uses a NumberPicker spinner. Therefore the code - // accommodates these two cases to be backwards compatible. - private final Button mAmPmButton; - - private final String[] mAmPmStrings; - - private boolean mIsEnabled = DEFAULT_ENABLED_STATE; - - // callbacks - private OnTimeChangedListener mOnTimeChangedListener; - - private Calendar mTempCalendar; - - private Locale mCurrentLocale; - - private boolean mHourWithTwoDigit; - private char mHourFormat; + private AttributeSet mAttrs; + private int mDefStyleAttr; + private int mDefStyleRes; + private Context mContext; /** * The callback interface used to indicate the time has been adjusted. @@ -131,345 +76,79 @@ public class TimePicker extends FrameLayout { this(context, attrs, R.attr.timePickerStyle); } - public TimePicker(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - // initialization based on locale - setCurrentLocale(Locale.getDefault()); - - // process style attributes - TypedArray attributesArray = context.obtainStyledAttributes( - attrs, R.styleable.TimePicker, defStyle, 0); - int layoutResourceId = attributesArray.getResourceId( - R.styleable.TimePicker_internalLayout, R.layout.time_picker); - attributesArray.recycle(); - - LayoutInflater inflater = (LayoutInflater) context.getSystemService( - Context.LAYOUT_INFLATER_SERVICE); - inflater.inflate(layoutResourceId, this, true); - - // hour - mHourSpinner = (NumberPicker) findViewById(R.id.hour); - mHourSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() { - public void onValueChange(NumberPicker spinner, int oldVal, int newVal) { - updateInputState(); - if (!is24HourView()) { - if ((oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) - || (oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1)) { - mIsAm = !mIsAm; - updateAmPmControl(); - } - } - onTimeChanged(); - } - }); - mHourSpinnerInput = (EditText) mHourSpinner.findViewById(R.id.numberpicker_input); - mHourSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); - - // divider (only for the new widget style) - mDivider = (TextView) findViewById(R.id.divider); - if (mDivider != null) { - setDividerText(); - } - - // minute - mMinuteSpinner = (NumberPicker) findViewById(R.id.minute); - mMinuteSpinner.setMinValue(0); - mMinuteSpinner.setMaxValue(59); - mMinuteSpinner.setOnLongPressUpdateInterval(100); - mMinuteSpinner.setFormatter(NumberPicker.getTwoDigitFormatter()); - mMinuteSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() { - public void onValueChange(NumberPicker spinner, int oldVal, int newVal) { - updateInputState(); - int minValue = mMinuteSpinner.getMinValue(); - int maxValue = mMinuteSpinner.getMaxValue(); - if (oldVal == maxValue && newVal == minValue) { - int newHour = mHourSpinner.getValue() + 1; - if (!is24HourView() && newHour == HOURS_IN_HALF_DAY) { - mIsAm = !mIsAm; - updateAmPmControl(); - } - mHourSpinner.setValue(newHour); - } else if (oldVal == minValue && newVal == maxValue) { - int newHour = mHourSpinner.getValue() - 1; - if (!is24HourView() && newHour == HOURS_IN_HALF_DAY - 1) { - mIsAm = !mIsAm; - updateAmPmControl(); - } - mHourSpinner.setValue(newHour); - } - onTimeChanged(); - } - }); - mMinuteSpinnerInput = (EditText) mMinuteSpinner.findViewById(R.id.numberpicker_input); - mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); - - /* Get the localized am/pm strings and use them in the spinner */ - mAmPmStrings = new DateFormatSymbols().getAmPmStrings(); - - // am/pm - View amPmView = findViewById(R.id.amPm); - if (amPmView instanceof Button) { - mAmPmSpinner = null; - mAmPmSpinnerInput = null; - mAmPmButton = (Button) amPmView; - mAmPmButton.setOnClickListener(new OnClickListener() { - public void onClick(View button) { - button.requestFocus(); - mIsAm = !mIsAm; - updateAmPmControl(); - onTimeChanged(); - } - }); - } else { - mAmPmButton = null; - mAmPmSpinner = (NumberPicker) amPmView; - mAmPmSpinner.setMinValue(0); - mAmPmSpinner.setMaxValue(1); - mAmPmSpinner.setDisplayedValues(mAmPmStrings); - mAmPmSpinner.setOnValueChangedListener(new OnValueChangeListener() { - public void onValueChange(NumberPicker picker, int oldVal, int newVal) { - updateInputState(); - picker.requestFocus(); - mIsAm = !mIsAm; - updateAmPmControl(); - onTimeChanged(); - } - }); - mAmPmSpinnerInput = (EditText) mAmPmSpinner.findViewById(R.id.numberpicker_input); - mAmPmSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE); - } - - if (isAmPmAtStart()) { - // Move the am/pm view to the beginning - ViewGroup amPmParent = (ViewGroup) findViewById(R.id.timePickerLayout); - amPmParent.removeView(amPmView); - amPmParent.addView(amPmView, 0); - // Swap layout margins if needed. They may be not symmetrical (Old Standard Theme for - // example and not for Holo Theme) - ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) amPmView.getLayoutParams(); - final int startMargin = lp.getMarginStart(); - final int endMargin = lp.getMarginEnd(); - if (startMargin != endMargin) { - lp.setMarginStart(endMargin); - lp.setMarginEnd(startMargin); - } - } - - getHourFormatData(); - - // update controls to initial state - updateHourControl(); - updateMinuteControl(); - updateAmPmControl(); - - setOnTimeChangedListener(NO_OP_CHANGE_LISTENER); - - // set to current time - setCurrentHour(mTempCalendar.get(Calendar.HOUR_OF_DAY)); - setCurrentMinute(mTempCalendar.get(Calendar.MINUTE)); - - if (!isEnabled()) { - setEnabled(false); - } - - // set the content descriptions - setContentDescriptions(); - - // If not explicitly specified this view is important for accessibility. - if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { - setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); - } + public TimePicker(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); } - private void getHourFormatData() { - final Locale defaultLocale = Locale.getDefault(); - final String bestDateTimePattern = DateFormat.getBestDateTimePattern(defaultLocale, - (mIs24HourView) ? "Hm" : "hm"); - final int lengthPattern = bestDateTimePattern.length(); - mHourWithTwoDigit = false; - char hourFormat = '\0'; - // Check if the returned pattern is single or double 'H', 'h', 'K', 'k'. We also save - // the hour format that we found. - for (int i = 0; i < lengthPattern; i++) { - final char c = bestDateTimePattern.charAt(i); - if (c == 'H' || c == 'h' || c == 'K' || c == 'k') { - mHourFormat = c; - if (i + 1 < lengthPattern && c == bestDateTimePattern.charAt(i + 1)) { - mHourWithTwoDigit = true; - } - break; - } - } - } + public TimePicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - private boolean isAmPmAtStart() { - final Locale defaultLocale = Locale.getDefault(); - final String bestDateTimePattern = DateFormat.getBestDateTimePattern(defaultLocale, - "hm" /* skeleton */); + mContext = context; + mAttrs = attrs; + mDefStyleAttr = defStyleAttr; + mDefStyleRes = defStyleRes; - return bestDateTimePattern.startsWith("a"); - } + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TimePicker, + mDefStyleAttr, mDefStyleRes); - @Override - public void setEnabled(boolean enabled) { - if (mIsEnabled == enabled) { - return; - } - super.setEnabled(enabled); - mMinuteSpinner.setEnabled(enabled); - if (mDivider != null) { - mDivider.setEnabled(enabled); - } - mHourSpinner.setEnabled(enabled); - if (mAmPmSpinner != null) { - mAmPmSpinner.setEnabled(enabled); - } else { - mAmPmButton.setEnabled(enabled); - } - mIsEnabled = enabled; + // Create the correct UI delegate. Default is the legacy one. + final boolean isLegacyMode = shouldForceLegacyMode() ? + true : a.getBoolean(R.styleable.TimePicker_legacyMode, true); + setLegacyMode(isLegacyMode); } - @Override - public boolean isEnabled() { - return mIsEnabled; + private boolean shouldForceLegacyMode() { + final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion; + return targetSdkVersion < KITKAT; } - @Override - protected void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - setCurrentLocale(newConfig.locale); + private TimePickerDelegate createLegacyUIDelegate(Context context, AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + return new LegacyTimePickerDelegate(this, context, attrs, defStyleAttr, defStyleRes); } - /** - * Sets the current locale. - * - * @param locale The current locale. - */ - private void setCurrentLocale(Locale locale) { - if (locale.equals(mCurrentLocale)) { - return; - } - mCurrentLocale = locale; - mTempCalendar = Calendar.getInstance(locale); + private TimePickerDelegate createNewUIDelegate(Context context, AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + return new android.widget.TimePickerDelegate(this, context, attrs, defStyleAttr, + defStyleRes); } /** - * Used to save / restore state of time picker + * @hide */ - private static class SavedState extends BaseSavedState { - - private final int mHour; - - private final int mMinute; - - private SavedState(Parcelable superState, int hour, int minute) { - super(superState); - mHour = hour; - mMinute = minute; - } - - private SavedState(Parcel in) { - super(in); - mHour = in.readInt(); - mMinute = in.readInt(); - } - - public int getHour() { - return mHour; - } - - public int getMinute() { - return mMinute; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - super.writeToParcel(dest, flags); - dest.writeInt(mHour); - dest.writeInt(mMinute); - } - - @SuppressWarnings({"unused", "hiding"}) - public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() { - public SavedState createFromParcel(Parcel in) { - return new SavedState(in); - } - - public SavedState[] newArray(int size) { - return new SavedState[size]; - } - }; - } - - @Override - protected Parcelable onSaveInstanceState() { - Parcelable superState = super.onSaveInstanceState(); - return new SavedState(superState, getCurrentHour(), getCurrentMinute()); - } - - @Override - protected void onRestoreInstanceState(Parcelable state) { - SavedState ss = (SavedState) state; - super.onRestoreInstanceState(ss.getSuperState()); - setCurrentHour(ss.getHour()); - setCurrentMinute(ss.getMinute()); + public void setLegacyMode(boolean isLegacyMode) { + removeAllViewsInLayout(); + mDelegate = isLegacyMode ? + createLegacyUIDelegate(mContext, mAttrs, mDefStyleAttr, mDefStyleRes) : + createNewUIDelegate(mContext, mAttrs, mDefStyleAttr, mDefStyleRes); } /** - * Set the callback that indicates the time has been adjusted by the user. - * - * @param onTimeChangedListener the callback, should not be null. + * Set the current hour. */ - public void setOnTimeChangedListener(OnTimeChangedListener onTimeChangedListener) { - mOnTimeChangedListener = onTimeChangedListener; + public void setCurrentHour(Integer currentHour) { + mDelegate.setCurrentHour(currentHour); } /** * @return The current hour in the range (0-23). */ public Integer getCurrentHour() { - int currentHour = mHourSpinner.getValue(); - if (is24HourView()) { - return currentHour; - } else if (mIsAm) { - return currentHour % HOURS_IN_HALF_DAY; - } else { - return (currentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY; - } + return mDelegate.getCurrentHour(); } /** - * Set the current hour. + * Set the current minute (0-59). */ - public void setCurrentHour(Integer currentHour) { - setCurrentHour(currentHour, true); + public void setCurrentMinute(Integer currentMinute) { + mDelegate.setCurrentMinute(currentMinute); } - private void setCurrentHour(Integer currentHour, boolean notifyTimeChanged) { - // why was Integer used in the first place? - if (currentHour == null || currentHour == getCurrentHour()) { - return; - } - if (!is24HourView()) { - // convert [0,23] ordinal to wall clock display - if (currentHour >= HOURS_IN_HALF_DAY) { - mIsAm = false; - if (currentHour > HOURS_IN_HALF_DAY) { - currentHour = currentHour - HOURS_IN_HALF_DAY; - } - } else { - mIsAm = true; - if (currentHour == 0) { - currentHour = HOURS_IN_HALF_DAY; - } - } - updateAmPmControl(); - } - mHourSpinner.setValue(currentHour); - if (notifyTimeChanged) { - onTimeChanged(); - } + /** + * @return The current minute. + */ + public Integer getCurrentMinute() { + return mDelegate.getCurrentMinute(); } /** @@ -478,223 +157,174 @@ public class TimePicker extends FrameLayout { * @param is24HourView True = 24 hour mode. False = AM/PM. */ public void setIs24HourView(Boolean is24HourView) { - if (mIs24HourView == is24HourView) { - return; - } - // cache the current hour since spinner range changes and BEFORE changing mIs24HourView!! - int currentHour = getCurrentHour(); - // Order is important here. - mIs24HourView = is24HourView; - getHourFormatData(); - updateHourControl(); - // set value after spinner range is updated - be aware that because mIs24HourView has - // changed then getCurrentHour() is not equal to the currentHour we cached before so - // explicitly ask for *not* propagating any onTimeChanged() - setCurrentHour(currentHour, false /* no onTimeChanged() */); - updateMinuteControl(); - updateAmPmControl(); + mDelegate.setIs24HourView(is24HourView); } /** * @return true if this is in 24 hour view else false. */ public boolean is24HourView() { - return mIs24HourView; + return mDelegate.is24HourView(); } /** - * @return The current minute. + * Set the callback that indicates the time has been adjusted by the user. + * + * @param onTimeChangedListener the callback, should not be null. */ - public Integer getCurrentMinute() { - return mMinuteSpinner.getValue(); + public void setOnTimeChangedListener(OnTimeChangedListener onTimeChangedListener) { + mDelegate.setOnTimeChangedListener(onTimeChangedListener); } - /** - * Set the current minute (0-59). - */ - public void setCurrentMinute(Integer currentMinute) { - if (currentMinute == getCurrentMinute()) { + @Override + public void setEnabled(boolean enabled) { + if (mDelegate.isEnabled() == enabled) { return; } - mMinuteSpinner.setValue(currentMinute); - onTimeChanged(); + super.setEnabled(enabled); + mDelegate.setEnabled(enabled); + } + + @Override + public boolean isEnabled() { + return mDelegate.isEnabled(); } /** - * The time separator is defined in the Unicode CLDR and cannot be supposed to be ":". - * - * See http://unicode.org/cldr/trac/browser/trunk/common/main - * - * We pass the correct "skeleton" depending on 12 or 24 hours view and then extract the - * separator as the character which is just after the hour marker in the returned pattern. + * @hide */ - private void setDividerText() { - final Locale defaultLocale = Locale.getDefault(); - final String skeleton = (mIs24HourView) ? "Hm" : "hm"; - final String bestDateTimePattern = DateFormat.getBestDateTimePattern(defaultLocale, - skeleton); - final String separatorText; - int hourIndex = bestDateTimePattern.lastIndexOf('H'); - if (hourIndex == -1) { - hourIndex = bestDateTimePattern.lastIndexOf('h'); - } - if (hourIndex == -1) { - // Default case - separatorText = ":"; - } else { - int minuteIndex = bestDateTimePattern.indexOf('m', hourIndex + 1); - if (minuteIndex == -1) { - separatorText = Character.toString(bestDateTimePattern.charAt(hourIndex + 1)); - } else { - separatorText = bestDateTimePattern.substring(hourIndex + 1, minuteIndex); - } - } - mDivider.setText(separatorText); + public void setShowDoneButton(boolean showDoneButton) { + mDelegate.setShowDoneButton(showDoneButton); + } + + /** + * @hide + */ + public void setDismissCallback(TimePickerDismissCallback callback) { + mDelegate.setDismissCallback(callback); } @Override public int getBaseline() { - return mHourSpinner.getBaseline(); + return mDelegate.getBaseline(); + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + mDelegate.onConfigurationChanged(newConfig); + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + return mDelegate.onSaveInstanceState(superState); + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + BaseSavedState ss = (BaseSavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + mDelegate.onRestoreInstanceState(ss); } @Override public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - onPopulateAccessibilityEvent(event); - return true; + return mDelegate.dispatchPopulateAccessibilityEvent(event); } @Override public void onPopulateAccessibilityEvent(AccessibilityEvent event) { super.onPopulateAccessibilityEvent(event); - - int flags = DateUtils.FORMAT_SHOW_TIME; - if (mIs24HourView) { - flags |= DateUtils.FORMAT_24HOUR; - } else { - flags |= DateUtils.FORMAT_12HOUR; - } - mTempCalendar.set(Calendar.HOUR_OF_DAY, getCurrentHour()); - mTempCalendar.set(Calendar.MINUTE, getCurrentMinute()); - String selectedDateUtterance = DateUtils.formatDateTime(mContext, - mTempCalendar.getTimeInMillis(), flags); - event.getText().add(selectedDateUtterance); + mDelegate.onPopulateAccessibilityEvent(event); } @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); - event.setClassName(TimePicker.class.getName()); + mDelegate.onInitializeAccessibilityEvent(event); } @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(TimePicker.class.getName()); + mDelegate.onInitializeAccessibilityNodeInfo(info); } - private void updateHourControl() { - if (is24HourView()) { - // 'k' means 1-24 hour - if (mHourFormat == 'k') { - mHourSpinner.setMinValue(1); - mHourSpinner.setMaxValue(24); - } else { - mHourSpinner.setMinValue(0); - mHourSpinner.setMaxValue(23); - } - } else { - // 'K' means 0-11 hour - if (mHourFormat == 'K') { - mHourSpinner.setMinValue(0); - mHourSpinner.setMaxValue(11); - } else { - mHourSpinner.setMinValue(1); - mHourSpinner.setMaxValue(12); - } - } - mHourSpinner.setFormatter(mHourWithTwoDigit ? NumberPicker.getTwoDigitFormatter() : null); - } + /** + * A delegate interface that defined the public API of the TimePicker. Allows different + * TimePicker implementations. This would need to be implemented by the TimePicker delegates + * for the real behavior. + */ + interface TimePickerDelegate { + void setCurrentHour(Integer currentHour); + Integer getCurrentHour(); - private void updateMinuteControl() { - if (is24HourView()) { - mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE); - } else { - mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); - } - } + void setCurrentMinute(Integer currentMinute); + Integer getCurrentMinute(); - private void updateAmPmControl() { - if (is24HourView()) { - if (mAmPmSpinner != null) { - mAmPmSpinner.setVisibility(View.GONE); - } else { - mAmPmButton.setVisibility(View.GONE); - } - } else { - int index = mIsAm ? Calendar.AM : Calendar.PM; - if (mAmPmSpinner != null) { - mAmPmSpinner.setValue(index); - mAmPmSpinner.setVisibility(View.VISIBLE); - } else { - mAmPmButton.setText(mAmPmStrings[index]); - mAmPmButton.setVisibility(View.VISIBLE); - } - } - sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); - } + void setIs24HourView(Boolean is24HourView); + boolean is24HourView(); - private void onTimeChanged() { - sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); - if (mOnTimeChangedListener != null) { - mOnTimeChangedListener.onTimeChanged(this, getCurrentHour(), getCurrentMinute()); - } + void setOnTimeChangedListener(OnTimeChangedListener onTimeChangedListener); + + void setEnabled(boolean enabled); + boolean isEnabled(); + + void setShowDoneButton(boolean showDoneButton); + void setDismissCallback(TimePickerDismissCallback callback); + + int getBaseline(); + + void onConfigurationChanged(Configuration newConfig); + + Parcelable onSaveInstanceState(Parcelable superState); + void onRestoreInstanceState(Parcelable state); + + boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event); + void onPopulateAccessibilityEvent(AccessibilityEvent event); + void onInitializeAccessibilityEvent(AccessibilityEvent event); + void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info); } - private void setContentDescriptions() { - // Minute - trySetContentDescription(mMinuteSpinner, R.id.increment, - R.string.time_picker_increment_minute_button); - trySetContentDescription(mMinuteSpinner, R.id.decrement, - R.string.time_picker_decrement_minute_button); - // Hour - trySetContentDescription(mHourSpinner, R.id.increment, - R.string.time_picker_increment_hour_button); - trySetContentDescription(mHourSpinner, R.id.decrement, - R.string.time_picker_decrement_hour_button); - // AM/PM - if (mAmPmSpinner != null) { - trySetContentDescription(mAmPmSpinner, R.id.increment, - R.string.time_picker_increment_set_pm_button); - trySetContentDescription(mAmPmSpinner, R.id.decrement, - R.string.time_picker_decrement_set_am_button); - } + /** + * A callback interface for dismissing the TimePicker when included into a Dialog + * + * @hide + */ + public static interface TimePickerDismissCallback { + void dismiss(TimePicker view, boolean isCancel, int hourOfDay, int minute); } - private void trySetContentDescription(View root, int viewId, int contDescResId) { - View target = root.findViewById(viewId); - if (target != null) { - target.setContentDescription(mContext.getString(contDescResId)); + /** + * An abstract class which can be used as a start for TimePicker implementations + */ + abstract static class AbstractTimePickerDelegate implements TimePickerDelegate { + // The delegator + protected TimePicker mDelegator; + + // The context + protected Context mContext; + + // The current locale + protected Locale mCurrentLocale; + + // Callbacks + protected OnTimeChangedListener mOnTimeChangedListener; + + public AbstractTimePickerDelegate(TimePicker delegator, Context context) { + mDelegator = delegator; + mContext = context; + + // initialization based on locale + setCurrentLocale(Locale.getDefault()); } - } - private void updateInputState() { - // Make sure that if the user changes the value and the IME is active - // for one of the inputs if this widget, the IME is closed. If the user - // changed the value via the IME and there is a next input the IME will - // be shown, otherwise the user chose another means of changing the - // value and having the IME up makes no sense. - InputMethodManager inputMethodManager = InputMethodManager.peekInstance(); - if (inputMethodManager != null) { - if (inputMethodManager.isActive(mHourSpinnerInput)) { - mHourSpinnerInput.clearFocus(); - inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); - } else if (inputMethodManager.isActive(mMinuteSpinnerInput)) { - mMinuteSpinnerInput.clearFocus(); - inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); - } else if (inputMethodManager.isActive(mAmPmSpinnerInput)) { - mAmPmSpinnerInput.clearFocus(); - inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); + public void setCurrentLocale(Locale locale) { + if (locale.equals(mCurrentLocale)) { + return; } + mCurrentLocale = locale; } } } diff --git a/core/java/android/widget/TimePickerDelegate.java b/core/java/android/widget/TimePickerDelegate.java new file mode 100644 index 0000000..79256e5 --- /dev/null +++ b/core/java/android/widget/TimePickerDelegate.java @@ -0,0 +1,1401 @@ +/* + * 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.widget; + +import android.animation.Keyframe; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.text.format.DateFormat; +import android.text.format.DateUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.util.TypedValue; +import android.view.HapticFeedbackConstants; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; + +import com.android.internal.R; + +import java.text.DateFormatSymbols; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Locale; + +/** + * A view for selecting the time of day, in either 24 hour or AM/PM mode. + */ +class TimePickerDelegate extends TimePicker.AbstractTimePickerDelegate implements + RadialTimePickerView.OnValueSelectedListener { + + private static final String TAG = "TimePickerDelegate"; + + // Index used by RadialPickerLayout + private static final int HOUR_INDEX = 0; + private static final int MINUTE_INDEX = 1; + + // NOT a real index for the purpose of what's showing. + private static final int AMPM_INDEX = 2; + + // Also NOT a real index, just used for keyboard mode. + private static final int ENABLE_PICKER_INDEX = 3; + + private static final int AM = 0; + private static final int PM = 1; + + private static final boolean DEFAULT_ENABLED_STATE = true; + private boolean mIsEnabled = DEFAULT_ENABLED_STATE; + + private static final int HOURS_IN_HALF_DAY = 12; + + // Delay in ms before starting the pulse animation + private static final int PULSE_ANIMATOR_DELAY = 300; + + // Duration in ms of the pulse animation + private static final int PULSE_ANIMATOR_DURATION = 544; + + private static int[] TEXT_APPEARANCE_TIME_LABEL_ATTR = + new int[] { R.attr.timePickerHeaderTimeLabelTextAppearance }; + + private final View mMainView; + private TextView mHourView; + private TextView mMinuteView; + private TextView mAmPmTextView; + private RadialTimePickerView mRadialTimePickerView; + private TextView mSeparatorView; + + private ViewGroup mLayoutButtons; + + private int mHeaderSelectedColor; + private int mHeaderUnSelectedColor; + private String mAmText; + private String mPmText; + + private boolean mAllowAutoAdvance; + private int mInitialHourOfDay; + private int mInitialMinute; + private boolean mIs24HourView; + + // For hardware IME input. + private char mPlaceholderText; + private String mDoublePlaceholderText; + private String mDeletedKeyFormat; + private boolean mInKbMode; + private ArrayList<Integer> mTypedTimes = new ArrayList<Integer>(); + private Node mLegalTimesTree; + private int mAmKeyCode; + private int mPmKeyCode; + + // For showing the done button when in a Dialog + private Button mDoneButton; + private boolean mShowDoneButton; + private TimePicker.TimePickerDismissCallback mDismissCallback; + + // Accessibility strings. + private String mHourPickerDescription; + private String mSelectHours; + private String mMinutePickerDescription; + private String mSelectMinutes; + + private Calendar mTempCalendar; + + public TimePickerDelegate(TimePicker delegator, Context context, AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + super(delegator, context); + + // process style attributes + final TypedArray a = mContext.obtainStyledAttributes(attrs, + R.styleable.TimePicker, defStyleAttr, defStyleRes); + + final Resources res = mContext.getResources(); + + mHourPickerDescription = res.getString(R.string.hour_picker_description); + mSelectHours = res.getString(R.string.select_hours); + mMinutePickerDescription = res.getString(R.string.minute_picker_description); + mSelectMinutes = res.getString(R.string.select_minutes); + + mHeaderSelectedColor = a.getColor(R.styleable.TimePicker_headerSelectedTextColor, + android.R.color.holo_blue_light); + + mHeaderUnSelectedColor = getUnselectedColor( + R.color.timepicker_default_text_color_holo_light); + if (mHeaderUnSelectedColor == -1) { + mHeaderUnSelectedColor = a.getColor(R.styleable.TimePicker_headerUnselectedTextColor, + R.color.timepicker_default_text_color_holo_light); + } + + final int headerBackgroundColor = a.getColor( + R.styleable.TimePicker_headerBackgroundColor, 0); + + final int layoutResourceId = a.getResourceId( + R.styleable.TimePicker_internalLayout, R.layout.time_picker_holo); + + a.recycle(); + + final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + + mMainView = inflater.inflate(layoutResourceId, null); + mDelegator.addView(mMainView); + + if (headerBackgroundColor != 0) { + RelativeLayout header = (RelativeLayout) mMainView.findViewById(R.id.time_header); + header.setBackgroundColor(headerBackgroundColor); + } + + mHourView = (TextView) mMainView.findViewById(R.id.hours); + mMinuteView = (TextView) mMainView.findViewById(R.id.minutes); + mAmPmTextView = (TextView) mMainView.findViewById(R.id.ampm_label); + mSeparatorView = (TextView) mMainView.findViewById(R.id.separator); + mRadialTimePickerView = (RadialTimePickerView) mMainView.findViewById(R.id.radial_picker); + + mLayoutButtons = (ViewGroup) mMainView.findViewById(R.id.layout_buttons); + mDoneButton = (Button) mMainView.findViewById(R.id.done_button); + + String[] amPmTexts = new DateFormatSymbols().getAmPmStrings(); + mAmText = amPmTexts[0]; + mPmText = amPmTexts[1]; + + setupListeners(); + + mAllowAutoAdvance = true; + + // Set up for keyboard mode. + mDoublePlaceholderText = res.getString(R.string.time_placeholder); + mDeletedKeyFormat = res.getString(R.string.deleted_key); + mPlaceholderText = mDoublePlaceholderText.charAt(0); + mAmKeyCode = mPmKeyCode = -1; + generateLegalTimesTree(); + + // Initialize with current time + final Calendar calendar = Calendar.getInstance(mCurrentLocale); + final int currentHour = calendar.get(Calendar.HOUR_OF_DAY); + final int currentMinute = calendar.get(Calendar.MINUTE); + initialize(currentHour, currentMinute, false /* 12h */, HOUR_INDEX, false); + } + + private int getUnselectedColor(int defColor) { + int result = -1; + final Resources.Theme theme = mContext.getTheme(); + final TypedValue outValue = new TypedValue(); + theme.resolveAttribute(R.attr.timePickerHeaderTimeLabelTextAppearance, outValue, true); + final int appearanceResId = outValue.resourceId; + TypedArray appearance = null; + if (appearanceResId != -1) { + appearance = theme.obtainStyledAttributes(appearanceResId, + com.android.internal.R.styleable.TextAppearance); + } + if (appearance != null) { + result = appearance.getColor( + com.android.internal.R.styleable.TextAppearance_textColor, defColor); + appearance.recycle(); + } + return result; + } + + private void initialize(int hourOfDay, int minute, boolean is24HourView, int index, + boolean showDoneButton) { + mInitialHourOfDay = hourOfDay; + mInitialMinute = minute; + mIs24HourView = is24HourView; + mInKbMode = false; + mShowDoneButton = showDoneButton; + updateUI(index); + } + + private void setupListeners() { + KeyboardListener keyboardListener = new KeyboardListener(); + mDelegator.setOnKeyListener(keyboardListener); + + mHourView.setOnKeyListener(keyboardListener); + mMinuteView.setOnKeyListener(keyboardListener); + mAmPmTextView.setOnKeyListener(keyboardListener); + mRadialTimePickerView.setOnValueSelectedListener(this); + mRadialTimePickerView.setOnKeyListener(keyboardListener); + + mHourView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + setCurrentItemShowing(HOUR_INDEX, true, false, true); + tryVibrate(); + } + }); + mMinuteView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + setCurrentItemShowing(MINUTE_INDEX, true, false, true); + tryVibrate(); + } + }); + mDoneButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (mInKbMode && isTypedTimeFullyLegal()) { + finishKbMode(false); + } else { + tryVibrate(); + } + if (mDismissCallback != null) { + mDismissCallback.dismiss(mDelegator, false, getCurrentHour(), + getCurrentMinute()); + } + } + }); + mDoneButton.setOnKeyListener(keyboardListener); + } + + private void updateUI(int index) { + // Update RadialPicker values + updateRadialPicker(index); + // Enable or disable the AM/PM view. + updateHeaderAmPm(); + // Show or hide Done button + updateDoneButton(); + // Update Hour and Minutes + updateHeaderHour(mInitialHourOfDay, true); + // Update time separator + updateHeaderSeparator(); + // Update Minutes + updateHeaderMinute(mInitialMinute); + // Invalidate everything + mDelegator.invalidate(); + } + + private void updateRadialPicker(int index) { + mRadialTimePickerView.initialize(mInitialHourOfDay, mInitialMinute, mIs24HourView); + setCurrentItemShowing(index, false, true, true); + } + + private int computeMaxWidthOfNumbers(int max) { + TextView tempView = new TextView(mContext); + TypedArray a = mContext.obtainStyledAttributes(TEXT_APPEARANCE_TIME_LABEL_ATTR); + final int textAppearanceResId = a.getResourceId(0, 0); + tempView.setTextAppearance(mContext, (textAppearanceResId != 0) ? + textAppearanceResId : R.style.TextAppearance_Holo_TimePicker_TimeLabel); + a.recycle(); + ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + tempView.setLayoutParams(lp); + int maxWidth = 0; + for (int minutes = 0; minutes < max; minutes++) { + final String text = String.format("%02d", minutes); + tempView.setText(text); + tempView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); + maxWidth = Math.max(maxWidth, tempView.getMeasuredWidth()); + } + return maxWidth; + } + + private void updateHeaderAmPm() { + if (mIs24HourView) { + mAmPmTextView.setVisibility(View.GONE); + } else { + mAmPmTextView.setVisibility(View.VISIBLE); + final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, + "hm"); + + boolean amPmOnLeft = bestDateTimePattern.startsWith("a"); + if (TextUtils.getLayoutDirectionFromLocale(mCurrentLocale) == + View.LAYOUT_DIRECTION_RTL) { + amPmOnLeft = !amPmOnLeft; + } + + RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) + mAmPmTextView.getLayoutParams(); + + if (amPmOnLeft) { + layoutParams.rightMargin = computeMaxWidthOfNumbers(12 /* for hours */); + layoutParams.removeRule(RelativeLayout.RIGHT_OF); + layoutParams.addRule(RelativeLayout.LEFT_OF, R.id.separator); + } else { + layoutParams.leftMargin = computeMaxWidthOfNumbers(60 /* for minutes */); + layoutParams.removeRule(RelativeLayout.LEFT_OF); + layoutParams.addRule(RelativeLayout.RIGHT_OF, R.id.separator); + } + + updateAmPmDisplay(mInitialHourOfDay < 12 ? AM : PM); + mAmPmTextView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + tryVibrate(); + int amOrPm = mRadialTimePickerView.getAmOrPm(); + if (amOrPm == AM) { + amOrPm = PM; + } else if (amOrPm == PM){ + amOrPm = AM; + } + updateAmPmDisplay(amOrPm); + mRadialTimePickerView.setAmOrPm(amOrPm); + } + }); + } + } + + private void updateDoneButton() { + mLayoutButtons.setVisibility(mShowDoneButton ? View.VISIBLE : View.GONE); + } + + /** + * Set the current hour. + */ + @Override + public void setCurrentHour(Integer currentHour) { + if (mInitialHourOfDay == currentHour) { + return; + } + mInitialHourOfDay = currentHour; + updateHeaderHour(currentHour, true /* accessibility announce */); + updateHeaderAmPm(); + mRadialTimePickerView.setCurrentHour(currentHour); + mRadialTimePickerView.setAmOrPm(mInitialHourOfDay < 12 ? AM : PM); + mDelegator.invalidate(); + onTimeChanged(); + } + + /** + * @return The current hour in the range (0-23). + */ + @Override + public Integer getCurrentHour() { + int currentHour = mRadialTimePickerView.getCurrentHour(); + if (mIs24HourView) { + return currentHour; + } else { + switch(mRadialTimePickerView.getAmOrPm()) { + case PM: + return (currentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY; + case AM: + default: + return currentHour % HOURS_IN_HALF_DAY; + } + } + } + + /** + * Set the current minute (0-59). + */ + @Override + public void setCurrentMinute(Integer currentMinute) { + if (mInitialMinute == currentMinute) { + return; + } + mInitialMinute = currentMinute; + updateHeaderMinute(currentMinute); + mRadialTimePickerView.setCurrentMinute(currentMinute); + mDelegator.invalidate(); + onTimeChanged(); + } + + /** + * @return The current minute. + */ + @Override + public Integer getCurrentMinute() { + return mRadialTimePickerView.getCurrentMinute(); + } + + /** + * Set whether in 24 hour or AM/PM mode. + * + * @param is24HourView True = 24 hour mode. False = AM/PM. + */ + @Override + public void setIs24HourView(Boolean is24HourView) { + if (is24HourView == mIs24HourView) { + return; + } + mIs24HourView = is24HourView; + generateLegalTimesTree(); + int hour = mRadialTimePickerView.getCurrentHour(); + mInitialHourOfDay = hour; + updateHeaderHour(hour, false /* no accessibility announce */); + updateHeaderAmPm(); + updateRadialPicker(mRadialTimePickerView.getCurrentItemShowing()); + mDelegator.invalidate(); + } + + /** + * @return true if this is in 24 hour view else false. + */ + @Override + public boolean is24HourView() { + return mIs24HourView; + } + + @Override + public void setOnTimeChangedListener(TimePicker.OnTimeChangedListener callback) { + mOnTimeChangedListener = callback; + } + + @Override + public void setEnabled(boolean enabled) { + mHourView.setEnabled(enabled); + mMinuteView.setEnabled(enabled); + mAmPmTextView.setEnabled(enabled); + mRadialTimePickerView.setEnabled(enabled); + mIsEnabled = enabled; + } + + @Override + public boolean isEnabled() { + return mIsEnabled; + } + + @Override + public void setShowDoneButton(boolean showDoneButton) { + mShowDoneButton = showDoneButton; + updateDoneButton(); + } + + @Override + public void setDismissCallback(TimePicker.TimePickerDismissCallback callback) { + mDismissCallback = callback; + } + + @Override + public int getBaseline() { + // does not support baseline alignment + return -1; + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + updateUI(mRadialTimePickerView.getCurrentItemShowing()); + } + + @Override + public Parcelable onSaveInstanceState(Parcelable superState) { + return new SavedState(superState, getCurrentHour(), getCurrentMinute(), + is24HourView(), inKbMode(), getTypedTimes(), getCurrentItemShowing(), + isShowDoneButton()); + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + setInKbMode(ss.inKbMode()); + setTypedTimes(ss.getTypesTimes()); + initialize(ss.getHour(), ss.getMinute(), ss.is24HourMode(), ss.getCurrentItemShowing(), + ss.isShowDoneButton()); + mRadialTimePickerView.invalidate(); + if (mInKbMode) { + tryStartingKbMode(-1); + mHourView.invalidate(); + } + } + + @Override + public void setCurrentLocale(Locale locale) { + super.setCurrentLocale(locale); + mTempCalendar = Calendar.getInstance(locale); + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + onPopulateAccessibilityEvent(event); + return true; + } + + @Override + public void onPopulateAccessibilityEvent(AccessibilityEvent event) { + int flags = DateUtils.FORMAT_SHOW_TIME; + if (mIs24HourView) { + flags |= DateUtils.FORMAT_24HOUR; + } else { + flags |= DateUtils.FORMAT_12HOUR; + } + mTempCalendar.set(Calendar.HOUR_OF_DAY, getCurrentHour()); + mTempCalendar.set(Calendar.MINUTE, getCurrentMinute()); + String selectedDate = DateUtils.formatDateTime(mContext, + mTempCalendar.getTimeInMillis(), flags); + event.getText().add(selectedDate); + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + event.setClassName(TimePicker.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + info.setClassName(TimePicker.class.getName()); + } + + /** + * Set whether in keyboard mode or not. + * + * @param inKbMode True means in keyboard mode. + */ + private void setInKbMode(boolean inKbMode) { + mInKbMode = inKbMode; + } + + /** + * @return true if in keyboard mode + */ + private boolean inKbMode() { + return mInKbMode; + } + + private void setTypedTimes(ArrayList<Integer> typeTimes) { + mTypedTimes = typeTimes; + } + + /** + * @return an array of typed times + */ + private ArrayList<Integer> getTypedTimes() { + return mTypedTimes; + } + + /** + * @return the index of the current item showing + */ + private int getCurrentItemShowing() { + return mRadialTimePickerView.getCurrentItemShowing(); + } + + private boolean isShowDoneButton() { + return mShowDoneButton; + } + + /** + * Propagate the time change + */ + private void onTimeChanged() { + mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); + if (mOnTimeChangedListener != null) { + mOnTimeChangedListener.onTimeChanged(mDelegator, + getCurrentHour(), getCurrentMinute()); + } + } + + /** + * Used to save / restore state of time picker + */ + private static class SavedState extends View.BaseSavedState { + + private final int mHour; + private final int mMinute; + private final boolean mIs24HourMode; + private final boolean mInKbMode; + private final ArrayList<Integer> mTypedTimes; + private final int mCurrentItemShowing; + private final boolean mShowDoneButton; + + private SavedState(Parcelable superState, int hour, int minute, boolean is24HourMode, + boolean isKbMode, ArrayList<Integer> typedTimes, + int currentItemShowing, boolean showDoneButton) { + super(superState); + mHour = hour; + mMinute = minute; + mIs24HourMode = is24HourMode; + mInKbMode = isKbMode; + mTypedTimes = typedTimes; + mCurrentItemShowing = currentItemShowing; + mShowDoneButton = showDoneButton; + } + + private SavedState(Parcel in) { + super(in); + mHour = in.readInt(); + mMinute = in.readInt(); + mIs24HourMode = (in.readInt() == 1); + mInKbMode = (in.readInt() == 1); + mTypedTimes = in.readArrayList(getClass().getClassLoader()); + mCurrentItemShowing = in.readInt(); + mShowDoneButton = (in.readInt() == 1); + } + + public int getHour() { + return mHour; + } + + public int getMinute() { + return mMinute; + } + + public boolean is24HourMode() { + return mIs24HourMode; + } + + public boolean inKbMode() { + return mInKbMode; + } + + public ArrayList<Integer> getTypesTimes() { + return mTypedTimes; + } + + public int getCurrentItemShowing() { + return mCurrentItemShowing; + } + + public boolean isShowDoneButton() { + return mShowDoneButton; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(mHour); + dest.writeInt(mMinute); + dest.writeInt(mIs24HourMode ? 1 : 0); + dest.writeInt(mInKbMode ? 1 : 0); + dest.writeList(mTypedTimes); + dest.writeInt(mCurrentItemShowing); + dest.writeInt(mShowDoneButton ? 1 : 0); + } + + @SuppressWarnings({"unused", "hiding"}) + public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + private void tryVibrate() { + mDelegator.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK); + } + + private void updateAmPmDisplay(int amOrPm) { + if (amOrPm == AM) { + mAmPmTextView.setText(mAmText); + mRadialTimePickerView.announceForAccessibility(mAmText); + } else if (amOrPm == PM){ + mAmPmTextView.setText(mPmText); + mRadialTimePickerView.announceForAccessibility(mPmText); + } else { + mAmPmTextView.setText(mDoublePlaceholderText); + } + } + + /** + * Called by the picker for updating the header display. + */ + @Override + public void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance) { + if (pickerIndex == HOUR_INDEX) { + updateHeaderHour(newValue, false); + String announcement = String.format("%d", newValue); + if (mAllowAutoAdvance && autoAdvance) { + setCurrentItemShowing(MINUTE_INDEX, true, true, false); + announcement += ". " + mSelectMinutes; + } else { + mRadialTimePickerView.setContentDescription( + mHourPickerDescription + ": " + newValue); + } + + mRadialTimePickerView.announceForAccessibility(announcement); + } else if (pickerIndex == MINUTE_INDEX){ + updateHeaderMinute(newValue); + mRadialTimePickerView.setContentDescription(mMinutePickerDescription + ": " + newValue); + } else if (pickerIndex == AMPM_INDEX) { + updateAmPmDisplay(newValue); + } else if (pickerIndex == ENABLE_PICKER_INDEX) { + if (!isTypedTimeFullyLegal()) { + mTypedTimes.clear(); + } + finishKbMode(true); + } + } + + private void updateHeaderHour(int value, boolean announce) { + final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, + (mIs24HourView) ? "Hm" : "hm"); + final int lengthPattern = bestDateTimePattern.length(); + boolean hourWithTwoDigit = false; + char hourFormat = '\0'; + // Check if the returned pattern is single or double 'H', 'h', 'K', 'k'. We also save + // the hour format that we found. + for (int i = 0; i < lengthPattern; i++) { + final char c = bestDateTimePattern.charAt(i); + if (c == 'H' || c == 'h' || c == 'K' || c == 'k') { + hourFormat = c; + if (i + 1 < lengthPattern && c == bestDateTimePattern.charAt(i + 1)) { + hourWithTwoDigit = true; + } + break; + } + } + final String format; + if (hourWithTwoDigit) { + format = "%02d"; + } else { + format = "%d"; + } + if (mIs24HourView) { + // 'k' means 1-24 hour + if (hourFormat == 'k' && value == 0) { + value = 24; + } + } else { + // 'K' means 0-11 hour + value = modulo12(value, hourFormat == 'K'); + } + CharSequence text = String.format(format, value); + mHourView.setText(text); + if (announce) { + mRadialTimePickerView.announceForAccessibility(text); + } + } + + private static int modulo12(int n, boolean startWithZero) { + int value = n % 12; + if (value == 0 && !startWithZero) { + value = 12; + } + return value; + } + + /** + * The time separator is defined in the Unicode CLDR and cannot be supposed to be ":". + * + * See http://unicode.org/cldr/trac/browser/trunk/common/main + * + * We pass the correct "skeleton" depending on 12 or 24 hours view and then extract the + * separator as the character which is just after the hour marker in the returned pattern. + */ + private void updateHeaderSeparator() { + final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, + (mIs24HourView) ? "Hm" : "hm"); + final String separatorText; + // See http://www.unicode.org/reports/tr35/tr35-dates.html for hour formats + final char[] hourFormats = {'H', 'h', 'K', 'k'}; + int hIndex = lastIndexOfAny(bestDateTimePattern, hourFormats); + if (hIndex == -1) { + // Default case + separatorText = ":"; + } else { + separatorText = Character.toString(bestDateTimePattern.charAt(hIndex + 1)); + } + mSeparatorView.setText(separatorText); + } + + static private int lastIndexOfAny(String str, char[] any) { + final int lengthAny = any.length; + if (lengthAny > 0) { + for (int i = str.length() - 1; i >= 0; i--) { + char c = str.charAt(i); + for (int j = 0; j < lengthAny; j++) { + if (c == any[j]) { + return i; + } + } + } + } + return -1; + } + + private void updateHeaderMinute(int value) { + if (value == 60) { + value = 0; + } + CharSequence text = String.format(mCurrentLocale, "%02d", value); + mRadialTimePickerView.announceForAccessibility(text); + mMinuteView.setText(text); + } + + /** + * Show either Hours or Minutes. + */ + private void setCurrentItemShowing(int index, boolean animateCircle, boolean delayLabelAnimate, + boolean announce) { + mRadialTimePickerView.setCurrentItemShowing(index, animateCircle); + + TextView labelToAnimate; + if (index == HOUR_INDEX) { + int hours = mRadialTimePickerView.getCurrentHour(); + if (!mIs24HourView) { + hours = hours % 12; + } + mRadialTimePickerView.setContentDescription(mHourPickerDescription + ": " + hours); + if (announce) { + mRadialTimePickerView.announceForAccessibility(mSelectHours); + } + labelToAnimate = mHourView; + } else { + int minutes = mRadialTimePickerView.getCurrentMinute(); + mRadialTimePickerView.setContentDescription(mMinutePickerDescription + ": " + minutes); + if (announce) { + mRadialTimePickerView.announceForAccessibility(mSelectMinutes); + } + labelToAnimate = mMinuteView; + } + + int hourColor = (index == HOUR_INDEX) ? mHeaderSelectedColor : mHeaderUnSelectedColor; + int minuteColor = (index == MINUTE_INDEX) ? mHeaderSelectedColor : mHeaderUnSelectedColor; + mHourView.setTextColor(hourColor); + mMinuteView.setTextColor(minuteColor); + + ObjectAnimator pulseAnimator = getPulseAnimator(labelToAnimate, 0.85f, 1.1f); + if (delayLabelAnimate) { + pulseAnimator.setStartDelay(PULSE_ANIMATOR_DELAY); + } + pulseAnimator.start(); + } + + /** + * For keyboard mode, processes key events. + * + * @param keyCode the pressed key. + * + * @return true if the key was successfully processed, false otherwise. + */ + private boolean processKeyUp(int keyCode) { + if (keyCode == KeyEvent.KEYCODE_ESCAPE || keyCode == KeyEvent.KEYCODE_BACK) { + if (mDismissCallback != null) { + mDismissCallback.dismiss(mDelegator, true, getCurrentHour(), getCurrentMinute()); + } + return true; + } else if (keyCode == KeyEvent.KEYCODE_TAB) { + if(mInKbMode) { + if (isTypedTimeFullyLegal()) { + finishKbMode(true); + } + return true; + } + } else if (keyCode == KeyEvent.KEYCODE_ENTER) { + if (mInKbMode) { + if (!isTypedTimeFullyLegal()) { + return true; + } + finishKbMode(false); + } + if (mOnTimeChangedListener != null) { + mOnTimeChangedListener.onTimeChanged(mDelegator, + mRadialTimePickerView.getCurrentHour(), + mRadialTimePickerView.getCurrentMinute()); + } + if (mDismissCallback != null) { + mDismissCallback.dismiss(mDelegator, false, getCurrentHour(), getCurrentMinute()); + } + return true; + } else if (keyCode == KeyEvent.KEYCODE_DEL) { + if (mInKbMode) { + if (!mTypedTimes.isEmpty()) { + int deleted = deleteLastTypedKey(); + String deletedKeyStr; + if (deleted == getAmOrPmKeyCode(AM)) { + deletedKeyStr = mAmText; + } else if (deleted == getAmOrPmKeyCode(PM)) { + deletedKeyStr = mPmText; + } else { + deletedKeyStr = String.format("%d", getValFromKeyCode(deleted)); + } + mRadialTimePickerView.announceForAccessibility( + String.format(mDeletedKeyFormat, deletedKeyStr)); + updateDisplay(true); + } + } + } else if (keyCode == KeyEvent.KEYCODE_0 || keyCode == KeyEvent.KEYCODE_1 + || keyCode == KeyEvent.KEYCODE_2 || keyCode == KeyEvent.KEYCODE_3 + || keyCode == KeyEvent.KEYCODE_4 || keyCode == KeyEvent.KEYCODE_5 + || keyCode == KeyEvent.KEYCODE_6 || keyCode == KeyEvent.KEYCODE_7 + || keyCode == KeyEvent.KEYCODE_8 || keyCode == KeyEvent.KEYCODE_9 + || (!mIs24HourView && + (keyCode == getAmOrPmKeyCode(AM) || keyCode == getAmOrPmKeyCode(PM)))) { + if (!mInKbMode) { + if (mRadialTimePickerView == null) { + // Something's wrong, because time picker should definitely not be null. + Log.e(TAG, "Unable to initiate keyboard mode, TimePicker was null."); + return true; + } + mTypedTimes.clear(); + tryStartingKbMode(keyCode); + return true; + } + // We're already in keyboard mode. + if (addKeyIfLegal(keyCode)) { + updateDisplay(false); + } + return true; + } + return false; + } + + /** + * Try to start keyboard mode with the specified key. + * + * @param keyCode The key to use as the first press. Keyboard mode will not be started if the + * key is not legal to start with. Or, pass in -1 to get into keyboard mode without a starting + * key. + */ + private void tryStartingKbMode(int keyCode) { + if (keyCode == -1 || addKeyIfLegal(keyCode)) { + mInKbMode = true; + mDoneButton.setEnabled(false); + updateDisplay(false); + mRadialTimePickerView.setInputEnabled(false); + } + } + + private boolean addKeyIfLegal(int keyCode) { + // If we're in 24hour mode, we'll need to check if the input is full. If in AM/PM mode, + // we'll need to see if AM/PM have been typed. + if ((mIs24HourView && mTypedTimes.size() == 4) || + (!mIs24HourView && isTypedTimeFullyLegal())) { + return false; + } + + mTypedTimes.add(keyCode); + if (!isTypedTimeLegalSoFar()) { + deleteLastTypedKey(); + return false; + } + + int val = getValFromKeyCode(keyCode); + mRadialTimePickerView.announceForAccessibility(String.format("%d", val)); + // Automatically fill in 0's if AM or PM was legally entered. + if (isTypedTimeFullyLegal()) { + if (!mIs24HourView && mTypedTimes.size() <= 3) { + mTypedTimes.add(mTypedTimes.size() - 1, KeyEvent.KEYCODE_0); + mTypedTimes.add(mTypedTimes.size() - 1, KeyEvent.KEYCODE_0); + } + mDoneButton.setEnabled(true); + } + + return true; + } + + /** + * Traverse the tree to see if the keys that have been typed so far are legal as is, + * or may become legal as more keys are typed (excluding backspace). + */ + private boolean isTypedTimeLegalSoFar() { + Node node = mLegalTimesTree; + for (int keyCode : mTypedTimes) { + node = node.canReach(keyCode); + if (node == null) { + return false; + } + } + return true; + } + + /** + * Check if the time that has been typed so far is completely legal, as is. + */ + private boolean isTypedTimeFullyLegal() { + if (mIs24HourView) { + // For 24-hour mode, the time is legal if the hours and minutes are each legal. Note: + // getEnteredTime() will ONLY call isTypedTimeFullyLegal() when NOT in 24hour mode. + int[] values = getEnteredTime(null); + return (values[0] >= 0 && values[1] >= 0 && values[1] < 60); + } else { + // For AM/PM mode, the time is legal if it contains an AM or PM, as those can only be + // legally added at specific times based on the tree's algorithm. + return (mTypedTimes.contains(getAmOrPmKeyCode(AM)) || + mTypedTimes.contains(getAmOrPmKeyCode(PM))); + } + } + + private int deleteLastTypedKey() { + int deleted = mTypedTimes.remove(mTypedTimes.size() - 1); + if (!isTypedTimeFullyLegal()) { + mDoneButton.setEnabled(false); + } + return deleted; + } + + /** + * Get out of keyboard mode. If there is nothing in typedTimes, revert to TimePicker's time. + * @param updateDisplays If true, update the displays with the relevant time. + */ + private void finishKbMode(boolean updateDisplays) { + mInKbMode = false; + if (!mTypedTimes.isEmpty()) { + int values[] = getEnteredTime(null); + mRadialTimePickerView.setCurrentHour(values[0]); + mRadialTimePickerView.setCurrentMinute(values[1]); + if (!mIs24HourView) { + mRadialTimePickerView.setAmOrPm(values[2]); + } + mTypedTimes.clear(); + } + if (updateDisplays) { + updateDisplay(false); + mRadialTimePickerView.setInputEnabled(true); + } + } + + /** + * Update the hours, minutes, and AM/PM displays with the typed times. If the typedTimes is + * empty, either show an empty display (filled with the placeholder text), or update from the + * timepicker's values. + * + * @param allowEmptyDisplay if true, then if the typedTimes is empty, use the placeholder text. + * Otherwise, revert to the timepicker's values. + */ + private void updateDisplay(boolean allowEmptyDisplay) { + if (!allowEmptyDisplay && mTypedTimes.isEmpty()) { + int hour = mRadialTimePickerView.getCurrentHour(); + int minute = mRadialTimePickerView.getCurrentMinute(); + updateHeaderHour(hour, true); + updateHeaderMinute(minute); + if (!mIs24HourView) { + updateAmPmDisplay(hour < 12 ? AM : PM); + } + setCurrentItemShowing(mRadialTimePickerView.getCurrentItemShowing(), true, true, true); + mDoneButton.setEnabled(true); + } else { + boolean[] enteredZeros = {false, false}; + int[] values = getEnteredTime(enteredZeros); + String hourFormat = enteredZeros[0] ? "%02d" : "%2d"; + String minuteFormat = (enteredZeros[1]) ? "%02d" : "%2d"; + String hourStr = (values[0] == -1) ? mDoublePlaceholderText : + String.format(hourFormat, values[0]).replace(' ', mPlaceholderText); + String minuteStr = (values[1] == -1) ? mDoublePlaceholderText : + String.format(minuteFormat, values[1]).replace(' ', mPlaceholderText); + mHourView.setText(hourStr); + mHourView.setTextColor(mHeaderUnSelectedColor); + mMinuteView.setText(minuteStr); + mMinuteView.setTextColor(mHeaderUnSelectedColor); + if (!mIs24HourView) { + updateAmPmDisplay(values[2]); + } + } + } + + private int getValFromKeyCode(int keyCode) { + switch (keyCode) { + case KeyEvent.KEYCODE_0: + return 0; + case KeyEvent.KEYCODE_1: + return 1; + case KeyEvent.KEYCODE_2: + return 2; + case KeyEvent.KEYCODE_3: + return 3; + case KeyEvent.KEYCODE_4: + return 4; + case KeyEvent.KEYCODE_5: + return 5; + case KeyEvent.KEYCODE_6: + return 6; + case KeyEvent.KEYCODE_7: + return 7; + case KeyEvent.KEYCODE_8: + return 8; + case KeyEvent.KEYCODE_9: + return 9; + default: + return -1; + } + } + + /** + * Get the currently-entered time, as integer values of the hours and minutes typed. + * + * @param enteredZeros A size-2 boolean array, which the caller should initialize, and which + * may then be used for the caller to know whether zeros had been explicitly entered as either + * hours of minutes. This is helpful for deciding whether to show the dashes, or actual 0's. + * + * @return A size-3 int array. The first value will be the hours, the second value will be the + * minutes, and the third will be either AM or PM. + */ + private int[] getEnteredTime(boolean[] enteredZeros) { + int amOrPm = -1; + int startIndex = 1; + if (!mIs24HourView && isTypedTimeFullyLegal()) { + int keyCode = mTypedTimes.get(mTypedTimes.size() - 1); + if (keyCode == getAmOrPmKeyCode(AM)) { + amOrPm = AM; + } else if (keyCode == getAmOrPmKeyCode(PM)){ + amOrPm = PM; + } + startIndex = 2; + } + int minute = -1; + int hour = -1; + for (int i = startIndex; i <= mTypedTimes.size(); i++) { + int val = getValFromKeyCode(mTypedTimes.get(mTypedTimes.size() - i)); + if (i == startIndex) { + minute = val; + } else if (i == startIndex+1) { + minute += 10 * val; + if (enteredZeros != null && val == 0) { + enteredZeros[1] = true; + } + } else if (i == startIndex+2) { + hour = val; + } else if (i == startIndex+3) { + hour += 10 * val; + if (enteredZeros != null && val == 0) { + enteredZeros[0] = true; + } + } + } + + int[] ret = {hour, minute, amOrPm}; + return ret; + } + + /** + * Get the keycode value for AM and PM in the current language. + */ + private int getAmOrPmKeyCode(int amOrPm) { + // Cache the codes. + if (mAmKeyCode == -1 || mPmKeyCode == -1) { + // Find the first character in the AM/PM text that is unique. + KeyCharacterMap kcm = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); + char amChar; + char pmChar; + for (int i = 0; i < Math.max(mAmText.length(), mPmText.length()); i++) { + amChar = mAmText.toLowerCase(mCurrentLocale).charAt(i); + pmChar = mPmText.toLowerCase(mCurrentLocale).charAt(i); + if (amChar != pmChar) { + KeyEvent[] events = kcm.getEvents(new char[]{amChar, pmChar}); + // There should be 4 events: a down and up for both AM and PM. + if (events != null && events.length == 4) { + mAmKeyCode = events[0].getKeyCode(); + mPmKeyCode = events[2].getKeyCode(); + } else { + Log.e(TAG, "Unable to find keycodes for AM and PM."); + } + break; + } + } + } + if (amOrPm == AM) { + return mAmKeyCode; + } else if (amOrPm == PM) { + return mPmKeyCode; + } + + return -1; + } + + /** + * Create a tree for deciding what keys can legally be typed. + */ + private void generateLegalTimesTree() { + // Create a quick cache of numbers to their keycodes. + final int k0 = KeyEvent.KEYCODE_0; + final int k1 = KeyEvent.KEYCODE_1; + final int k2 = KeyEvent.KEYCODE_2; + final int k3 = KeyEvent.KEYCODE_3; + final int k4 = KeyEvent.KEYCODE_4; + final int k5 = KeyEvent.KEYCODE_5; + final int k6 = KeyEvent.KEYCODE_6; + final int k7 = KeyEvent.KEYCODE_7; + final int k8 = KeyEvent.KEYCODE_8; + final int k9 = KeyEvent.KEYCODE_9; + + // The root of the tree doesn't contain any numbers. + mLegalTimesTree = new Node(); + if (mIs24HourView) { + // We'll be re-using these nodes, so we'll save them. + Node minuteFirstDigit = new Node(k0, k1, k2, k3, k4, k5); + Node minuteSecondDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); + // The first digit must be followed by the second digit. + minuteFirstDigit.addChild(minuteSecondDigit); + + // The first digit may be 0-1. + Node firstDigit = new Node(k0, k1); + mLegalTimesTree.addChild(firstDigit); + + // When the first digit is 0-1, the second digit may be 0-5. + Node secondDigit = new Node(k0, k1, k2, k3, k4, k5); + firstDigit.addChild(secondDigit); + // We may now be followed by the first minute digit. E.g. 00:09, 15:58. + secondDigit.addChild(minuteFirstDigit); + + // When the first digit is 0-1, and the second digit is 0-5, the third digit may be 6-9. + Node thirdDigit = new Node(k6, k7, k8, k9); + // The time must now be finished. E.g. 0:55, 1:08. + secondDigit.addChild(thirdDigit); + + // When the first digit is 0-1, the second digit may be 6-9. + secondDigit = new Node(k6, k7, k8, k9); + firstDigit.addChild(secondDigit); + // We must now be followed by the first minute digit. E.g. 06:50, 18:20. + secondDigit.addChild(minuteFirstDigit); + + // The first digit may be 2. + firstDigit = new Node(k2); + mLegalTimesTree.addChild(firstDigit); + + // When the first digit is 2, the second digit may be 0-3. + secondDigit = new Node(k0, k1, k2, k3); + firstDigit.addChild(secondDigit); + // We must now be followed by the first minute digit. E.g. 20:50, 23:09. + secondDigit.addChild(minuteFirstDigit); + + // When the first digit is 2, the second digit may be 4-5. + secondDigit = new Node(k4, k5); + firstDigit.addChild(secondDigit); + // We must now be followd by the last minute digit. E.g. 2:40, 2:53. + secondDigit.addChild(minuteSecondDigit); + + // The first digit may be 3-9. + firstDigit = new Node(k3, k4, k5, k6, k7, k8, k9); + mLegalTimesTree.addChild(firstDigit); + // We must now be followed by the first minute digit. E.g. 3:57, 8:12. + firstDigit.addChild(minuteFirstDigit); + } else { + // We'll need to use the AM/PM node a lot. + // Set up AM and PM to respond to "a" and "p". + Node ampm = new Node(getAmOrPmKeyCode(AM), getAmOrPmKeyCode(PM)); + + // The first hour digit may be 1. + Node firstDigit = new Node(k1); + mLegalTimesTree.addChild(firstDigit); + // We'll allow quick input of on-the-hour times. E.g. 1pm. + firstDigit.addChild(ampm); + + // When the first digit is 1, the second digit may be 0-2. + Node secondDigit = new Node(k0, k1, k2); + firstDigit.addChild(secondDigit); + // Also for quick input of on-the-hour times. E.g. 10pm, 12am. + secondDigit.addChild(ampm); + + // When the first digit is 1, and the second digit is 0-2, the third digit may be 0-5. + Node thirdDigit = new Node(k0, k1, k2, k3, k4, k5); + secondDigit.addChild(thirdDigit); + // The time may be finished now. E.g. 1:02pm, 1:25am. + thirdDigit.addChild(ampm); + + // When the first digit is 1, the second digit is 0-2, and the third digit is 0-5, + // the fourth digit may be 0-9. + Node fourthDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); + thirdDigit.addChild(fourthDigit); + // The time must be finished now. E.g. 10:49am, 12:40pm. + fourthDigit.addChild(ampm); + + // When the first digit is 1, and the second digit is 0-2, the third digit may be 6-9. + thirdDigit = new Node(k6, k7, k8, k9); + secondDigit.addChild(thirdDigit); + // The time must be finished now. E.g. 1:08am, 1:26pm. + thirdDigit.addChild(ampm); + + // When the first digit is 1, the second digit may be 3-5. + secondDigit = new Node(k3, k4, k5); + firstDigit.addChild(secondDigit); + + // When the first digit is 1, and the second digit is 3-5, the third digit may be 0-9. + thirdDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); + secondDigit.addChild(thirdDigit); + // The time must be finished now. E.g. 1:39am, 1:50pm. + thirdDigit.addChild(ampm); + + // The hour digit may be 2-9. + firstDigit = new Node(k2, k3, k4, k5, k6, k7, k8, k9); + mLegalTimesTree.addChild(firstDigit); + // We'll allow quick input of on-the-hour-times. E.g. 2am, 5pm. + firstDigit.addChild(ampm); + + // When the first digit is 2-9, the second digit may be 0-5. + secondDigit = new Node(k0, k1, k2, k3, k4, k5); + firstDigit.addChild(secondDigit); + + // When the first digit is 2-9, and the second digit is 0-5, the third digit may be 0-9. + thirdDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); + secondDigit.addChild(thirdDigit); + // The time must be finished now. E.g. 2:57am, 9:30pm. + thirdDigit.addChild(ampm); + } + } + + /** + * Simple node class to be used for traversal to check for legal times. + * mLegalKeys represents the keys that can be typed to get to the node. + * mChildren are the children that can be reached from this node. + */ + private class Node { + private int[] mLegalKeys; + private ArrayList<Node> mChildren; + + public Node(int... legalKeys) { + mLegalKeys = legalKeys; + mChildren = new ArrayList<Node>(); + } + + public void addChild(Node child) { + mChildren.add(child); + } + + public boolean containsKey(int key) { + for (int i = 0; i < mLegalKeys.length; i++) { + if (mLegalKeys[i] == key) { + return true; + } + } + return false; + } + + public Node canReach(int key) { + if (mChildren == null) { + return null; + } + for (Node child : mChildren) { + if (child.containsKey(key)) { + return child; + } + } + return null; + } + } + + private class KeyboardListener implements View.OnKeyListener { + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + if (event.getAction() == KeyEvent.ACTION_UP) { + return processKeyUp(keyCode); + } + return false; + } + } + + /** + * Render an animator to pulsate a view in place. + * + * @param labelToAnimate the view to pulsate. + * @return The animator object. Use .start() to begin. + */ + private static ObjectAnimator getPulseAnimator(View labelToAnimate, float decreaseRatio, + float increaseRatio) { + final Keyframe k0 = Keyframe.ofFloat(0f, 1f); + final Keyframe k1 = Keyframe.ofFloat(0.275f, decreaseRatio); + final Keyframe k2 = Keyframe.ofFloat(0.69f, increaseRatio); + final Keyframe k3 = Keyframe.ofFloat(1f, 1f); + + PropertyValuesHolder scaleX = PropertyValuesHolder.ofKeyframe("scaleX", k0, k1, k2, k3); + PropertyValuesHolder scaleY = PropertyValuesHolder.ofKeyframe("scaleY", k0, k1, k2, k3); + ObjectAnimator pulseAnimator = + ObjectAnimator.ofPropertyValuesHolder(labelToAnimate, scaleX, scaleY); + pulseAnimator.setDuration(PULSE_ANIMATOR_DURATION); + + return pulseAnimator; + } +} diff --git a/core/java/android/widget/Toast.java b/core/java/android/widget/Toast.java index e38dfa7..bf5e49b 100644 --- a/core/java/android/widget/Toast.java +++ b/core/java/android/widget/Toast.java @@ -16,6 +16,7 @@ package android.widget; +import android.annotation.IntDef; import android.app.INotificationManager; import android.app.ITransientNotification; import android.content.Context; @@ -29,11 +30,13 @@ import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; -import android.view.View.OnClickListener; import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * A toast is a view containing a quick little message for the user. The toast class * helps you create and show those. @@ -61,6 +64,11 @@ public class Toast { static final String TAG = "Toast"; static final boolean localLOGV = false; + /** @hide */ + @IntDef({LENGTH_SHORT, LENGTH_LONG}) + @Retention(RetentionPolicy.SOURCE) + public @interface Duration {} + /** * Show the view or text notification for a short period of time. This time * could be user-definable. This is the default. @@ -152,7 +160,7 @@ public class Toast { * @see #LENGTH_SHORT * @see #LENGTH_LONG */ - public void setDuration(int duration) { + public void setDuration(@Duration int duration) { mDuration = duration; } @@ -160,6 +168,7 @@ public class Toast { * Return the duration. * @see #setDuration */ + @Duration public int getDuration() { return mDuration; } @@ -237,7 +246,7 @@ public class Toast { * {@link #LENGTH_LONG} * */ - public static Toast makeText(Context context, CharSequence text, int duration) { + public static Toast makeText(Context context, CharSequence text, @Duration int duration) { Toast result = new Toast(context); LayoutInflater inflate = (LayoutInflater) @@ -263,7 +272,7 @@ public class Toast { * * @throws Resources.NotFoundException if the resource can't be found. */ - public static Toast makeText(Context context, int resId, int duration) + public static Toast makeText(Context context, int resId, @Duration int duration) throws Resources.NotFoundException { return makeText(context, context.getResources().getText(resId), duration); } diff --git a/core/java/android/widget/ToggleButton.java b/core/java/android/widget/ToggleButton.java index cedc777..28519d1 100644 --- a/core/java/android/widget/ToggleButton.java +++ b/core/java/android/widget/ToggleButton.java @@ -16,7 +16,6 @@ package android.widget; - import android.content.Context; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; @@ -25,8 +24,6 @@ import android.util.AttributeSet; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; -import com.android.internal.R; - /** * Displays checked/unchecked states as a button * with a "light" indicator and by default accompanied with the text "ON" or "OFF". @@ -46,13 +43,12 @@ public class ToggleButton extends CompoundButton { private static final int NO_ALPHA = 0xFF; private float mDisabledAlpha; - - public ToggleButton(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - TypedArray a = - context.obtainStyledAttributes( - attrs, com.android.internal.R.styleable.ToggleButton, defStyle, 0); + + public ToggleButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.ToggleButton, defStyleAttr, defStyleRes); mTextOn = a.getText(com.android.internal.R.styleable.ToggleButton_textOn); mTextOff = a.getText(com.android.internal.R.styleable.ToggleButton_textOff); mDisabledAlpha = a.getFloat(com.android.internal.R.styleable.ToggleButton_disabledAlpha, 0.5f); @@ -60,6 +56,10 @@ public class ToggleButton extends CompoundButton { a.recycle(); } + public ToggleButton(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + public ToggleButton(Context context, AttributeSet attrs) { this(context, attrs, com.android.internal.R.attr.buttonStyleToggle); } diff --git a/core/java/android/widget/Toolbar.java b/core/java/android/widget/Toolbar.java new file mode 100644 index 0000000..075feba --- /dev/null +++ b/core/java/android/widget/Toolbar.java @@ -0,0 +1,1048 @@ +/* + * Copyright (C) 2014 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.widget; + +import android.annotation.NonNull; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewDebug; +import android.view.ViewGroup; +import com.android.internal.R; + +import java.util.ArrayList; +import java.util.List; + +/** + * A standard toolbar for use within application content. + * + * <p>A Toolbar is a generalization of {@link android.app.ActionBar action bars} for use + * within application layouts. While an action bar is traditionally part of an + * {@link android.app.Activity Activity's} opaque window decor controlled by the framework, + * a Toolbar may be placed at any arbitrary level of nesting within a view hierarchy. + * An application may choose to designate a Toolbar as the action bar for an Activity + * using the {@link android.app.Activity#setActionBar(Toolbar) setActionBar()} method.</p> + * + * <p>Toolbar supports a more focused feature set than ActionBar. From start to end, a toolbar + * may contain a combination of the following optional elements: + * + * <ul> + * <li><em>A navigation button.</em> This may be an Up arrow, navigation menu toggle, close, + * collapse, done or another glyph of the app's choosing. This button should always be used + * to access other navigational destinations within the container of the Toolbar and + * its signified content or otherwise leave the current context signified by the Toolbar.</li> + * <li><em>A branded logo image.</em> This may extend to the height of the bar and can be + * arbitrarily wide.</li> + * <li><em>A title and subtitle.</em> The title should be a signpost for the Toolbar's current + * position in the navigation hierarchy and the content contained there. The subtitle, + * if present should indicate any extended information about the current content. + * If an app uses a logo image it should strongly consider omitting a title and subtitle.</li> + * <li><em>One or more custom views.</em> The application may add arbitrary child views + * to the Toolbar. They will appear at this position within the layout. If a child view's + * {@link LayoutParams} indicates a {@link Gravity} value of + * {@link Gravity#CENTER_HORIZONTAL CENTER_HORIZONTAL} the view will attempt to center + * within the available space remaining in the Toolbar after all other elements have been + * measured.</li> + * <li><em>An {@link ActionMenuView action menu}.</em> The menu of actions will pin to the + * end of the Toolbar offering a few + * <a href="http://developer.android.com/design/patterns/actionbar.html#ActionButtons"> + * frequent, important or typical</a> actions along with an optional overflow menu for + * additional actions.</li> + * </ul> + * </p> + * + * <p>In modern Android UIs developers should lean more on a visually distinct color scheme for + * toolbars than on their application icon. The use of application icon plus title as a standard + * layout is discouraged on API 21 devices and newer.</p> + */ +public class Toolbar extends ViewGroup { + private ActionMenuView mMenuView; + private TextView mTitleTextView; + private TextView mSubtitleTextView; + private ImageButton mNavButtonView; + private ImageView mLogoView; + + private int mTitleTextAppearance; + private int mSubtitleTextAppearance; + private int mTitleMarginStart; + private int mTitleMarginEnd; + private int mTitleMarginTop; + private int mTitleMarginBottom; + + private int mGravity = Gravity.START | Gravity.CENTER_VERTICAL; + + private CharSequence mTitleText; + private CharSequence mSubtitleText; + + // Clear me after use. + private final ArrayList<View> mTempViews = new ArrayList<View>(); + + private OnMenuItemClickListener mOnMenuItemClickListener; + + private final ActionMenuView.OnMenuItemClickListener mMenuViewItemClickListener = + new ActionMenuView.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + if (mOnMenuItemClickListener != null) { + return mOnMenuItemClickListener.onMenuItemClick(item); + } + return false; + } + }; + + public Toolbar(Context context) { + this(context, null); + } + + public Toolbar(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.toolbarStyle); + } + + public Toolbar(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public Toolbar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Toolbar, + defStyleAttr, defStyleRes); + + mTitleTextAppearance = a.getResourceId(R.styleable.Toolbar_titleTextAppearance, 0); + mSubtitleTextAppearance = a.getResourceId(R.styleable.Toolbar_subtitleTextAppearance, 0); + mGravity = a.getInteger(R.styleable.Toolbar_gravity, mGravity); + mTitleMarginStart = mTitleMarginEnd = Math.max(0, a.getDimensionPixelOffset( + R.styleable.Toolbar_titleMargins, -1)); + + final int marginStart = a.getDimensionPixelOffset(R.styleable.Toolbar_titleMarginStart, -1); + if (marginStart >= 0) { + mTitleMarginStart = marginStart; + } + + final int marginEnd = a.getDimensionPixelOffset(R.styleable.Toolbar_titleMarginEnd, -1); + if (marginEnd >= 0) { + mTitleMarginEnd = marginEnd; + } + + final int marginTop = a.getDimensionPixelOffset(R.styleable.Toolbar_titleMarginTop, -1); + if (marginTop >= 0) { + mTitleMarginTop = marginTop; + } + + final int marginBottom = a.getDimensionPixelOffset(R.styleable.Toolbar_titleMarginBottom, + -1); + if (marginBottom >= 0) { + mTitleMarginBottom = marginBottom; + } + + final CharSequence title = a.getText(R.styleable.Toolbar_title); + if (!TextUtils.isEmpty(title)) { + setTitle(title); + } + + final CharSequence subtitle = a.getText(R.styleable.Toolbar_subtitle); + if (!TextUtils.isEmpty(subtitle)) { + setSubtitle(title); + } + a.recycle(); + } + + /** + * Set a logo drawable from a resource id. + * + * <p>This drawable should generally take the place of title text. The logo cannot be + * clicked. Apps using a logo should also supply a description using + * {@link #setLogoDescription(int)}.</p> + * + * @param resId ID of a drawable resource + */ + public void setLogo(int resId) { + setLogo(getContext().getDrawable(resId)); + } + + /** + * Set a logo drawable. + * + * <p>This drawable should generally take the place of title text. The logo cannot be + * clicked. Apps using a logo should also supply a description using + * {@link #setLogoDescription(int)}.</p> + * + * @param drawable Drawable to use as a logo + */ + public void setLogo(Drawable drawable) { + if (drawable != null) { + if (mLogoView == null) { + mLogoView = new ImageView(getContext()); + } + if (mLogoView.getParent() == null) { + addSystemView(mLogoView); + } + } else if (mLogoView != null && mLogoView.getParent() != null) { + removeView(mLogoView); + } + if (mLogoView != null) { + mLogoView.setImageDrawable(drawable); + } + } + + /** + * Return the current logo drawable. + * + * @return The current logo drawable + * @see #setLogo(int) + * @see #setLogo(android.graphics.drawable.Drawable) + */ + public Drawable getLogo() { + return mLogoView != null ? mLogoView.getDrawable() : null; + } + + /** + * Set a description of the toolbar's logo. + * + * <p>This description will be used for accessibility or other similar descriptions + * of the UI.</p> + * + * @param resId String resource id + */ + public void setLogoDescription(int resId) { + setLogoDescription(getContext().getText(resId)); + } + + /** + * Set a description of the toolbar's logo. + * + * <p>This description will be used for accessibility or other similar descriptions + * of the UI.</p> + * + * @param description Description to set + */ + public void setLogoDescription(CharSequence description) { + if (!TextUtils.isEmpty(description) && mLogoView == null) { + mLogoView = new ImageView(getContext()); + } + if (mLogoView != null) { + mLogoView.setContentDescription(description); + } + } + + /** + * Return the description of the toolbar's logo. + * + * @return A description of the logo + */ + public CharSequence getLogoDescription() { + return mLogoView != null ? mLogoView.getContentDescription() : null; + } + + /** + * Return the current title displayed in the toolbar. + * + * @return The current title + */ + public CharSequence getTitle() { + return mTitleText; + } + + /** + * Set the title of this toolbar. + * + * <p>A title should be used as the anchor for a section of content. It should + * describe or name the content being viewed.</p> + * + * @param resId Resource ID of a string to set as the title + */ + public void setTitle(int resId) { + setTitle(getContext().getText(resId)); + } + + /** + * Set the title of this toolbar. + * + * <p>A title should be used as the anchor for a section of content. It should + * describe or name the content being viewed.</p> + * + * @param title Title to set + */ + public void setTitle(CharSequence title) { + if (!TextUtils.isEmpty(title)) { + if (mTitleTextView == null) { + final Context context = getContext(); + mTitleTextView = new TextView(context); + mTitleTextView.setTextAppearance(context, mTitleTextAppearance); + } + if (mTitleTextView.getParent() == null) { + addSystemView(mTitleTextView); + } + } else if (mTitleTextView != null && mTitleTextView.getParent() != null) { + removeView(mTitleTextView); + } + if (mTitleTextView != null) { + mTitleTextView.setText(title); + } + mTitleText = title; + } + + /** + * Return the subtitle of this toolbar. + * + * @return The current subtitle + */ + public CharSequence getSubtitle() { + return mSubtitleText; + } + + /** + * Set the subtitle of this toolbar. + * + * <p>Subtitles should express extended information about the current content.</p> + * + * @param resId String resource ID + */ + public void setSubtitle(int resId) { + setSubtitle(getContext().getText(resId)); + } + + /** + * Set the subtitle of this toolbar. + * + * <p>Subtitles should express extended information about the current content.</p> + * + * @param subtitle Subtitle to set + */ + public void setSubtitle(CharSequence subtitle) { + if (!TextUtils.isEmpty(subtitle)) { + if (mSubtitleTextView == null) { + final Context context = getContext(); + mSubtitleTextView = new TextView(context); + mSubtitleTextView.setTextAppearance(context, mSubtitleTextAppearance); + } + if (mSubtitleTextView.getParent() == null) { + addSystemView(mSubtitleTextView); + } + } else if (mSubtitleTextView != null && mSubtitleTextView.getParent() != null) { + removeView(mSubtitleTextView); + } + if (mSubtitleTextView != null) { + mSubtitleTextView.setText(subtitle); + } + mSubtitleText = subtitle; + } + + /** + * Set the icon to use for the toolbar's navigation button. + * + * <p>The navigation button appears at the start of the toolbar if present. Setting an icon + * will make the navigation button visible.</p> + * + * <p>If you use a navigation icon you should also set a description for its action using + * {@link #setNavigationDescription(int)}. This is used for accessibility and tooltips.</p> + * + * @param resId Resource ID of a drawable to set + */ + public void setNavigationIcon(int resId) { + setNavigationIcon(getContext().getDrawable(resId)); + } + + /** + * Set the icon to use for the toolbar's navigation button. + * + * <p>The navigation button appears at the start of the toolbar if present. Setting an icon + * will make the navigation button visible.</p> + * + * <p>If you use a navigation icon you should also set a description for its action using + * {@link #setNavigationDescription(int)}. This is used for accessibility and tooltips.</p> + * + * @param icon Drawable to set + */ + public void setNavigationIcon(Drawable icon) { + if (icon != null) { + ensureNavButtonView(); + if (mNavButtonView.getParent() == null) { + addSystemView(mNavButtonView); + } + } else if (mNavButtonView != null && mNavButtonView.getParent() != null) { + removeView(mNavButtonView); + } + if (mNavButtonView != null) { + mNavButtonView.setImageDrawable(icon); + } + } + + /** + * Return the current drawable used as the navigation icon. + * + * @return The navigation icon drawable + */ + public Drawable getNavigationIcon() { + return mNavButtonView != null ? mNavButtonView.getDrawable() : null; + } + + /** + * Set a description for the navigation button. + * + * <p>This description string is used for accessibility, tooltips and other facilities + * to improve discoverability.</p> + * + * @param resId Resource ID of a string to set + */ + public void setNavigationDescription(int resId) { + setNavigationDescription(getContext().getText(resId)); + } + + /** + * Set a description for the navigation button. + * + * <p>This description string is used for accessibility, tooltips and other facilities + * to improve discoverability.</p> + * + * @param description String to set as the description + */ + public void setNavigationDescription(CharSequence description) { + if (!TextUtils.isEmpty(description)) { + ensureNavButtonView(); + } + if (mNavButtonView != null) { + mNavButtonView.setContentDescription(description); + } + } + + /** + * Set a listener to respond to navigation events. + * + * <p>This listener will be called whenever the user clicks the navigation button + * at the start of the toolbar. An icon must be set for the navigation button to appear.</p> + * + * @param listener Listener to set + * @see #setNavigationIcon(android.graphics.drawable.Drawable) + */ + public void setNavigationOnClickListener(OnClickListener listener) { + ensureNavButtonView(); + mNavButtonView.setOnClickListener(listener); + } + + /** + * Return the Menu shown in the toolbar. + * + * <p>Applications that wish to populate the toolbar's menu can do so from here. To use + * an XML menu resource, use {@link #inflateMenu(int)}.</p> + * + * @return The toolbar's Menu + */ + public Menu getMenu() { + if (mMenuView == null) { + mMenuView = new ActionMenuView(getContext()); + mMenuView.setOnMenuItemClickListener(mMenuViewItemClickListener); + addSystemView(mMenuView); + } + return mMenuView.getMenu(); + } + + private MenuInflater getMenuInflater() { + return new MenuInflater(getContext()); + } + + /** + * Inflate a menu resource into this toolbar. + * + * <p>Inflate an XML menu resource into this toolbar. Existing items in the menu will not + * be modified or removed.</p> + * + * @param resId ID of a menu resource to inflate + */ + public void inflateMenu(int resId) { + getMenuInflater().inflate(resId, getMenu()); + } + + /** + * Set a listener to respond to menu item click events. + * + * <p>This listener will be invoked whenever a user selects a menu item from + * the action buttons presented at the end of the toolbar or the associated overflow.</p> + * + * @param listener Listener to set + */ + public void setOnMenuItemClickListener(OnMenuItemClickListener listener) { + mOnMenuItemClickListener = listener; + } + + private void ensureNavButtonView() { + if (mNavButtonView == null) { + mNavButtonView = new ImageButton(getContext(), null, R.attr.borderlessButtonStyle); + } + } + + private void addSystemView(View v) { + final LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT); + lp.mViewType = LayoutParams.SYSTEM; + addView(v, lp); + } + + @Override + protected Parcelable onSaveInstanceState() { + SavedState state = new SavedState(super.onSaveInstanceState()); + return state; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + final SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width = 0; + int height = 0; + int childState = 0; + + // System views measure first. + + if (shouldLayout(mNavButtonView)) { + measureChildWithMargins(mNavButtonView, widthMeasureSpec, width, heightMeasureSpec, 0); + width += mNavButtonView.getMeasuredWidth() + getHorizontalMargins(mNavButtonView); + height = Math.max(height, mNavButtonView.getMeasuredHeight() + + getVerticalMargins(mNavButtonView)); + childState = combineMeasuredStates(childState, mNavButtonView.getMeasuredState()); + } + + if (shouldLayout(mMenuView)) { + measureChildWithMargins(mMenuView, widthMeasureSpec, width, heightMeasureSpec, 0); + width += mMenuView.getMeasuredWidth() + getHorizontalMargins(mMenuView); + height = Math.max(height, mMenuView.getMeasuredHeight() + + getVerticalMargins(mMenuView)); + childState = combineMeasuredStates(childState, mMenuView.getMeasuredState()); + } + + if (shouldLayout(mLogoView)) { + measureChildWithMargins(mLogoView, widthMeasureSpec, width, heightMeasureSpec, 0); + width += mLogoView.getMeasuredWidth() + getHorizontalMargins(mLogoView); + height = Math.max(height, mLogoView.getMeasuredHeight() + + getVerticalMargins(mLogoView)); + childState = combineMeasuredStates(childState, mLogoView.getMeasuredState()); + } + + int titleWidth = 0; + int titleHeight = 0; + final int titleVertMargins = mTitleMarginTop + mTitleMarginBottom; + final int titleHorizMargins = mTitleMarginStart + mTitleMarginEnd; + if (shouldLayout(mTitleTextView)) { + measureChildWithMargins(mTitleTextView, widthMeasureSpec, width + titleHorizMargins, + heightMeasureSpec, titleVertMargins); + titleWidth = mTitleTextView.getMeasuredWidth() + getHorizontalMargins(mTitleTextView); + titleHeight = mTitleTextView.getMeasuredHeight() + getVerticalMargins(mTitleTextView); + childState = combineMeasuredStates(childState, mTitleTextView.getMeasuredState()); + } + if (shouldLayout(mSubtitleTextView)) { + measureChildWithMargins(mSubtitleTextView, widthMeasureSpec, width + titleHorizMargins, + heightMeasureSpec, titleHeight + titleVertMargins); + titleWidth = Math.max(titleWidth, mSubtitleTextView.getMeasuredWidth() + + getHorizontalMargins(mSubtitleTextView)); + titleHeight += mSubtitleTextView.getMeasuredHeight() + + getVerticalMargins(mSubtitleTextView); + childState = combineMeasuredStates(childState, mSubtitleTextView.getMeasuredState()); + } + + width += titleWidth; + height = Math.max(height, titleHeight); + + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (lp.mViewType == LayoutParams.SYSTEM || !shouldLayout(child)) { + // We already got all system views above. Skip them and GONE views. + continue; + } + + measureChildWithMargins(child, widthMeasureSpec, width, heightMeasureSpec, 0); + width += child.getMeasuredWidth() + getHorizontalMargins(child); + height = Math.max(height, child.getMeasuredHeight() + getVerticalMargins(child)); + childState = combineMeasuredStates(childState, child.getMeasuredState()); + } + + // Measurement already took padding into account for available space for the children, + // add it in for the final size. + width += getPaddingLeft() + getPaddingRight(); + height += getPaddingTop() + getPaddingBottom(); + + final int measuredWidth = resolveSizeAndState( + Math.max(width, getSuggestedMinimumWidth()), + widthMeasureSpec, childState & MEASURED_STATE_MASK); + final int measuredHeight = resolveSizeAndState( + Math.max(height, getSuggestedMinimumHeight()), + heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT); + setMeasuredDimension(measuredWidth, measuredHeight); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + final boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL; + final int width = getWidth(); + final int height = getHeight(); + final int paddingLeft = getPaddingLeft(); + final int paddingRight = getPaddingRight(); + final int paddingTop = getPaddingTop(); + final int paddingBottom = getPaddingBottom(); + int left = paddingLeft; + int right = width - paddingRight; + + if (shouldLayout(mNavButtonView)) { + if (isRtl) { + right = layoutChildRight(mNavButtonView, right); + } else { + left = layoutChildLeft(mNavButtonView, left); + } + } + + if (shouldLayout(mMenuView)) { + if (isRtl) { + left = layoutChildLeft(mMenuView, left); + } else { + right = layoutChildRight(mMenuView, right); + } + } + + if (shouldLayout(mLogoView)) { + if (isRtl) { + right = layoutChildRight(mLogoView, right); + } else { + left = layoutChildLeft(mLogoView, left); + } + } + + final boolean layoutTitle = shouldLayout(mTitleTextView); + final boolean layoutSubtitle = shouldLayout(mSubtitleTextView); + int titleHeight = 0; + if (layoutTitle) { + final LayoutParams lp = (LayoutParams) mTitleTextView.getLayoutParams(); + titleHeight += lp.topMargin + mTitleTextView.getMeasuredHeight() + lp.bottomMargin; + } + if (layoutSubtitle) { + final LayoutParams lp = (LayoutParams) mSubtitleTextView.getLayoutParams(); + titleHeight += lp.bottomMargin + mTitleTextView.getMeasuredHeight() + lp.bottomMargin; + } + + if (layoutTitle || layoutSubtitle) { + int titleTop; + switch (mGravity & Gravity.VERTICAL_GRAVITY_MASK) { + case Gravity.TOP: + titleTop = getPaddingTop(); + break; + default: + case Gravity.CENTER_VERTICAL: + final View child = layoutTitle ? mTitleTextView : mSubtitleTextView; + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + final int space = height - paddingTop - paddingBottom; + int spaceAbove = (space - titleHeight) / 2; + if (spaceAbove < lp.topMargin + mTitleMarginTop) { + spaceAbove = lp.topMargin + mTitleMarginTop; + } else { + final int spaceBelow = height - paddingBottom - titleHeight - + spaceAbove - paddingTop; + if (spaceBelow < lp.bottomMargin + mTitleMarginBottom) { + spaceAbove = Math.max(0, spaceAbove - + (lp.bottomMargin + mTitleMarginBottom - spaceBelow)); + } + } + titleTop = paddingTop + spaceAbove; + break; + case Gravity.BOTTOM: + titleTop = height - paddingBottom - titleHeight; + break; + } + if (isRtl) { + int titleRight = right; + int subtitleRight = right; + titleTop += mTitleMarginTop; + if (layoutTitle) { + final LayoutParams lp = (LayoutParams) mTitleTextView.getLayoutParams(); + titleRight -= lp.rightMargin + mTitleMarginStart; + titleTop += lp.topMargin; + final int titleLeft = titleRight - mTitleTextView.getMeasuredWidth(); + final int titleBottom = titleTop + mTitleTextView.getMeasuredHeight(); + mTitleTextView.layout(titleLeft, titleTop, titleRight, titleBottom); + titleRight = titleLeft - lp.leftMargin - mTitleMarginEnd; + titleTop = titleBottom + lp.bottomMargin; + } + if (layoutSubtitle) { + final LayoutParams lp = (LayoutParams) mSubtitleTextView.getLayoutParams(); + subtitleRight -= lp.rightMargin + mTitleMarginStart; + titleTop += lp.topMargin; + final int subtitleLeft = subtitleRight - mSubtitleTextView.getMeasuredWidth(); + final int subtitleBottom = titleTop + mSubtitleTextView.getMeasuredHeight(); + mSubtitleTextView.layout(subtitleLeft, titleTop, subtitleRight, subtitleBottom); + subtitleRight = subtitleRight - lp.leftMargin - mTitleMarginEnd; + titleTop = subtitleBottom + lp.bottomMargin; + } + right = Math.max(titleRight, subtitleRight); + } else { + int titleLeft = left; + int subtitleLeft = left; + titleTop += mTitleMarginTop; + if (layoutTitle) { + final LayoutParams lp = (LayoutParams) mTitleTextView.getLayoutParams(); + titleLeft += lp.leftMargin + mTitleMarginStart; + titleTop += lp.topMargin; + final int titleRight = titleLeft + mTitleTextView.getMeasuredWidth(); + final int titleBottom = titleTop + mTitleTextView.getMeasuredHeight(); + mTitleTextView.layout(titleLeft, titleTop, titleRight, titleBottom); + titleLeft = titleRight + lp.rightMargin + mTitleMarginEnd; + titleTop = titleBottom + lp.bottomMargin; + } + if (layoutSubtitle) { + final LayoutParams lp = (LayoutParams) mSubtitleTextView.getLayoutParams(); + subtitleLeft += lp.leftMargin + mTitleMarginStart; + titleTop += lp.topMargin; + final int subtitleRight = subtitleLeft + mSubtitleTextView.getMeasuredWidth(); + final int subtitleBottom = titleTop + mSubtitleTextView.getMeasuredHeight(); + mSubtitleTextView.layout(subtitleLeft, titleTop, subtitleRight, subtitleBottom); + subtitleLeft = subtitleRight + lp.rightMargin + mTitleMarginEnd; + titleTop = subtitleBottom + lp.bottomMargin; + } + left = Math.max(titleLeft, subtitleLeft); + } + } + + // Get all remaining children sorted for layout. This is all prepared + // such that absolute layout direction can be used below. + + addCustomViewsWithGravity(mTempViews, Gravity.LEFT); + final int leftViewsCount = mTempViews.size(); + for (int i = 0; i < leftViewsCount; i++) { + left = layoutChildLeft(getChildAt(i), left); + } + + addCustomViewsWithGravity(mTempViews, Gravity.RIGHT); + final int rightViewsCount = mTempViews.size(); + for (int i = 0; i < rightViewsCount; i++) { + right = layoutChildRight(getChildAt(i), right); + } + + // Centered views try to center with respect to the whole bar, but views pinned + // to the left or right can push the mass of centered views to one side or the other. + addCustomViewsWithGravity(mTempViews, Gravity.CENTER); + final int centerViewsWidth = getViewListMeasuredWidth(mTempViews); + final int parentCenter = paddingLeft + (width - paddingLeft - paddingRight) / 2; + final int halfCenterViewsWidth = centerViewsWidth / 2; + int centerLeft = parentCenter - halfCenterViewsWidth; + final int centerRight = centerLeft + centerViewsWidth; + if (centerLeft < left) { + centerLeft = left; + } else if (centerRight > right) { + centerLeft -= centerRight - right; + } + + final int centerViewsCount = mTempViews.size(); + for (int i = 0; i < centerViewsCount; i++) { + centerLeft = layoutChildLeft(getChildAt(i), centerLeft); + } + mTempViews.clear(); + } + + private int getViewListMeasuredWidth(List<View> views) { + int width = 0; + final int count = views.size(); + for (int i = 0; i < count; i++) { + final View v = views.get(i); + final LayoutParams lp = (LayoutParams) v.getLayoutParams(); + width += lp.leftMargin + v.getMeasuredWidth() + lp.rightMargin; + } + return width; + } + + private int layoutChildLeft(View child, int left) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + left += lp.leftMargin; + int top = getChildTop(child); + child.layout(left, top, left + child.getMeasuredWidth(), top + child.getMeasuredHeight()); + left += lp.rightMargin; + return left; + } + + private int layoutChildRight(View child, int right) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + right -= lp.rightMargin; + int top = getChildTop(child); + child.layout(right - child.getMeasuredWidth(), top, right, top + child.getMeasuredHeight()); + right -= lp.leftMargin; + return right; + } + + private int getChildTop(View child) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + switch (getChildVerticalGravity(lp.gravity)) { + case Gravity.TOP: + return getPaddingTop(); + + case Gravity.BOTTOM: + return getPaddingBottom() - child.getMeasuredHeight() - lp.bottomMargin; + + default: + case Gravity.CENTER_VERTICAL: + final int paddingTop = getPaddingTop(); + final int paddingBottom = getPaddingBottom(); + final int height = getHeight(); + final int childHeight = child.getMeasuredHeight(); + final int space = height - paddingTop - paddingBottom; + int spaceAbove = (space - childHeight) / 2; + if (spaceAbove < lp.topMargin) { + spaceAbove = lp.topMargin; + } else { + final int spaceBelow = height - paddingBottom - childHeight - + spaceAbove - paddingTop; + if (spaceBelow < lp.bottomMargin) { + spaceAbove = Math.max(0, spaceAbove - (lp.bottomMargin - spaceBelow)); + } + } + return paddingTop + spaceAbove; + } + } + + private int getChildVerticalGravity(int gravity) { + final int vgrav = gravity & Gravity.VERTICAL_GRAVITY_MASK; + switch (vgrav) { + case Gravity.TOP: + case Gravity.BOTTOM: + case Gravity.CENTER_VERTICAL: + return vgrav; + default: + return mGravity & Gravity.VERTICAL_GRAVITY_MASK; + } + } + + /** + * Prepare a list of non-SYSTEM child views. If the layout direction is RTL + * this will be in reverse child order. + * + * @param views List to populate. It will be cleared before use. + * @param gravity Horizontal gravity to match against + */ + private void addCustomViewsWithGravity(List<View> views, int gravity) { + final boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL; + final int childCount = getChildCount(); + final int absGrav = Gravity.getAbsoluteGravity(gravity, getLayoutDirection()); + + views.clear(); + + if (isRtl) { + for (int i = childCount - 1; i >= 0; i--) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (lp.mViewType != LayoutParams.SYSTEM && shouldLayout(child) && + getChildHorizontalGravity(lp.gravity) == absGrav) { + views.add(child); + } + + } + } else { + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (lp.mViewType != LayoutParams.SYSTEM && shouldLayout(child) && + getChildHorizontalGravity(lp.gravity) == absGrav) { + views.add(child); + } + } + } + } + + private int getChildHorizontalGravity(int gravity) { + final int ld = getLayoutDirection(); + final int absGrav = Gravity.getAbsoluteGravity(gravity, ld); + final int hGrav = absGrav & Gravity.HORIZONTAL_GRAVITY_MASK; + switch (hGrav) { + case Gravity.LEFT: + case Gravity.RIGHT: + case Gravity.CENTER_HORIZONTAL: + return hGrav; + default: + return ld == LAYOUT_DIRECTION_RTL ? Gravity.RIGHT : Gravity.LEFT; + } + } + + private boolean shouldLayout(View view) { + return view != null && view.getParent() == this && view.getVisibility() != GONE; + } + + private int getHorizontalMargins(View v) { + final MarginLayoutParams mlp = (MarginLayoutParams) v.getLayoutParams(); + return mlp.getMarginStart() + mlp.getMarginEnd(); + } + + private int getVerticalMargins(View v) { + final MarginLayoutParams mlp = (MarginLayoutParams) v.getLayoutParams(); + return mlp.topMargin + mlp.bottomMargin; + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + return super.generateLayoutParams(attrs); + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + if (p instanceof LayoutParams) { + return new LayoutParams((LayoutParams) p); + } else if (p instanceof MarginLayoutParams) { + return new LayoutParams((MarginLayoutParams) p); + } else { + return new LayoutParams(p); + } + } + + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return super.checkLayoutParams(p) && p instanceof LayoutParams; + } + + private static boolean isCustomView(View child) { + return ((LayoutParams) child.getLayoutParams()).mViewType == LayoutParams.CUSTOM; + } + + /** + * Interface responsible for receiving menu item click events if the items themselves + * do not have individual item click listeners. + */ + public interface OnMenuItemClickListener { + /** + * This method will be invoked when a menu item is clicked if the item itself did + * not already handle the event. + * + * @param item {@link MenuItem} that was clicked + * @return <code>true</code> if the event was handled, <code>false</code> otherwise. + */ + public boolean onMenuItemClick(MenuItem item); + } + + /** + * Layout information for child views of Toolbars. + * + * @attr ref android.R.styleable#Toolbar_LayoutParams_layout_gravity + */ + public static class LayoutParams extends MarginLayoutParams { + /** + * Gravity for the view associated with these LayoutParams. + * + * @see android.view.Gravity + */ + @ViewDebug.ExportedProperty(category = "layout", mapping = { + @ViewDebug.IntToString(from = -1, to = "NONE"), + @ViewDebug.IntToString(from = Gravity.NO_GRAVITY, to = "NONE"), + @ViewDebug.IntToString(from = Gravity.TOP, to = "TOP"), + @ViewDebug.IntToString(from = Gravity.BOTTOM, to = "BOTTOM"), + @ViewDebug.IntToString(from = Gravity.LEFT, to = "LEFT"), + @ViewDebug.IntToString(from = Gravity.RIGHT, to = "RIGHT"), + @ViewDebug.IntToString(from = Gravity.START, to = "START"), + @ViewDebug.IntToString(from = Gravity.END, to = "END"), + @ViewDebug.IntToString(from = Gravity.CENTER_VERTICAL, to = "CENTER_VERTICAL"), + @ViewDebug.IntToString(from = Gravity.FILL_VERTICAL, to = "FILL_VERTICAL"), + @ViewDebug.IntToString(from = Gravity.CENTER_HORIZONTAL, to = "CENTER_HORIZONTAL"), + @ViewDebug.IntToString(from = Gravity.FILL_HORIZONTAL, to = "FILL_HORIZONTAL"), + @ViewDebug.IntToString(from = Gravity.CENTER, to = "CENTER"), + @ViewDebug.IntToString(from = Gravity.FILL, to = "FILL") + }) + public int gravity = Gravity.NO_GRAVITY; + + static final int CUSTOM = 0; + static final int SYSTEM = 1; + + int mViewType = CUSTOM; + + public LayoutParams(@NonNull Context c, AttributeSet attrs) { + super(c, attrs); + + TypedArray a = c.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.Toolbar_LayoutParams); + gravity = a.getInt( + com.android.internal.R.styleable.Toolbar_LayoutParams_layout_gravity, + Gravity.NO_GRAVITY); + a.recycle(); + } + + public LayoutParams(int width, int height) { + super(width, height); + this.gravity = Gravity.CENTER_VERTICAL | Gravity.START; + } + + public LayoutParams(int width, int height, int gravity) { + super(width, height); + this.gravity = gravity; + } + + public LayoutParams(int gravity) { + this(WRAP_CONTENT, MATCH_PARENT, gravity); + } + + public LayoutParams(LayoutParams source) { + super(source); + + this.gravity = source.gravity; + } + + public LayoutParams(MarginLayoutParams source) { + super(source); + } + + public LayoutParams(ViewGroup.LayoutParams source) { + super(source); + } + } + + static class SavedState extends BaseSavedState { + public SavedState(Parcel source) { + super(source); + } + + public SavedState(Parcelable superState) { + super(superState); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + } + + public static final Creator<SavedState> CREATOR = new Creator<SavedState>() { + + @Override + public SavedState createFromParcel(Parcel source) { + return new SavedState(source); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } +} diff --git a/core/java/android/widget/TwoLineListItem.java b/core/java/android/widget/TwoLineListItem.java index f7e5266..5606c60 100644 --- a/core/java/android/widget/TwoLineListItem.java +++ b/core/java/android/widget/TwoLineListItem.java @@ -56,11 +56,15 @@ public class TwoLineListItem extends RelativeLayout { this(context, attrs, 0); } - public TwoLineListItem(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public TwoLineListItem(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public TwoLineListItem(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.TwoLineListItem, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.TwoLineListItem, defStyleAttr, defStyleRes); a.recycle(); } diff --git a/core/java/android/widget/VideoView.java b/core/java/android/widget/VideoView.java index d57b739..f23c64f 100644 --- a/core/java/android/widget/VideoView.java +++ b/core/java/android/widget/VideoView.java @@ -19,7 +19,6 @@ package android.widget; import android.app.AlertDialog; 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; @@ -127,8 +126,12 @@ public class VideoView extends SurfaceView initVideoView(); } - public VideoView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public VideoView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public VideoView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); initVideoView(); } @@ -297,11 +300,8 @@ public class VideoView extends SurfaceView // not ready for playback just yet, will try again later return; } - // Tell the music playback service to pause - // TODO: these constants need to be published somewhere in the framework. - Intent i = new Intent("com.android.music.musicservicecommand"); - i.putExtra("command", "pause"); - mContext.sendBroadcast(i); + AudioManager am = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); + am.requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); // we shouldn't clear the target state, because somebody might have // called start() previously diff --git a/core/java/android/widget/ZoomButton.java b/core/java/android/widget/ZoomButton.java index af17c94..715e868 100644 --- a/core/java/android/widget/ZoomButton.java +++ b/core/java/android/widget/ZoomButton.java @@ -49,8 +49,12 @@ public class ZoomButton extends ImageButton implements OnLongClickListener { this(context, attrs, 0); } - public ZoomButton(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public ZoomButton(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ZoomButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); mHandler = new Handler(); setOnLongClickListener(this); } diff --git a/core/java/android/widget/ZoomButtonsController.java b/core/java/android/widget/ZoomButtonsController.java index 50c803b..f7e9648 100644 --- a/core/java/android/widget/ZoomButtonsController.java +++ b/core/java/android/widget/ZoomButtonsController.java @@ -32,7 +32,6 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; -import android.view.ViewParent; import android.view.ViewRootImpl; import android.view.WindowManager; import android.view.View.OnClickListener; |