diff options
Diffstat (limited to 'core/java')
728 files changed, 64945 insertions, 19373 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..3c3df01 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(); @@ -775,7 +933,93 @@ public abstract class ActionBar { public void setHomeActionContentDescription(int resId) { } /** + * Enable hiding the action bar on content scroll. + * + * <p>If enabled, the action bar will scroll out of sight along with a + * {@link View#setNestedScrollingEnabled(boolean) nested scrolling child} view's content. + * The action bar must be in {@link Window#FEATURE_ACTION_BAR_OVERLAY overlay mode} + * to enable hiding on content scroll.</p> + * + * <p>When partially scrolled off screen the action bar is considered + * {@link #hide() hidden}. A call to {@link #show() show} will cause it to return to full view. + * </p> + * @param hideOnContentScroll true to enable hiding on content scroll. + */ + public void setHideOnContentScrollEnabled(boolean hideOnContentScroll) { + if (hideOnContentScroll) { + throw new UnsupportedOperationException("Hide on content scroll is not supported in " + + "this action bar configuration."); + } + } + + /** + * Return whether the action bar is configured to scroll out of sight along with + * a {@link View#setNestedScrollingEnabled(boolean) nested scrolling child}. + * + * @return true if hide-on-content-scroll is enabled + * @see #setHideOnContentScrollEnabled(boolean) + */ + public boolean isHideOnContentScrollEnabled() { + return false; + } + + /** + * Return the current vertical offset of the action bar. + * + * <p>The action bar's current hide offset is the distance that the action bar is currently + * scrolled offscreen in pixels. The valid range is 0 (fully visible) to the action bar's + * current measured {@link #getHeight() height} (fully invisible).</p> + * + * @return The action bar's offset toward its fully hidden state in pixels + */ + public int getHideOffset() { + return 0; + } + + /** + * Set the current hide offset of the action bar. + * + * <p>The action bar's current hide offset is the distance that the action bar is currently + * scrolled offscreen in pixels. The valid range is 0 (fully visible) to the action bar's + * current measured {@link #getHeight() height} (fully invisible).</p> + * + * @param offset The action bar's offset toward its fully hidden state in pixels. + */ + public void setHideOffset(int offset) { + if (offset != 0) { + throw new UnsupportedOperationException("Setting an explicit action bar hide offset " + + "is not supported in this action bar configuration."); + } + } + + /** @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 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 +1052,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 +1208,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 +1254,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 +1291,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 +1305,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..8981c88 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.TransitionManager; import android.util.ArrayMap; import android.util.SuperNotCalledException; -import com.android.internal.app.ActionBarImpl; +import android.widget.Toolbar; +import com.android.internal.app.IVoiceInteractor; +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,9 +724,11 @@ 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 VoiceInteractor mVoiceInteractor; + private CharSequence mTitle; private int mTitleColor = 0; @@ -763,6 +775,8 @@ public class Activity extends ContextThemeWrapper private Thread mUiThread; final Handler mHandler = new Handler(); + private ActivityOptions mCalledActivityOptions; + private EnterTransitionCoordinator mEnterTransitionCoordinator; /** Return the intent that started this activity. */ public Intent getIntent() { @@ -852,6 +866,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 +897,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,18 +1025,21 @@ 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()); } + if (mEnterTransitionCoordinator != null) { + mEnterTransitionCoordinator.readyToEnter(); + } mCalled = true; } /** * 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 @@ -1095,6 +1113,7 @@ public class Activity extends ContextThemeWrapper protected void onResume() { if (DEBUG_LIFECYCLE) Slog.v(TAG, "onResume " + this); getApplication().dispatchActivityResumed(this); + mCalledActivityOptions = null; mCalled = true; } @@ -1118,6 +1137,23 @@ public class Activity extends ContextThemeWrapper } /** + * Check whether this activity is running as part of a voice interaction with the user. + * If true, it should perform its interaction with the user through the + * {@link VoiceInteractor} returned by {@link #getVoiceInteractor}. + */ + public boolean isVoiceInteraction() { + return mVoiceInteractor != null; + } + + /** + * Retrieve the active {@link VoiceInteractor} that the user is going through to + * interact with this activity. + */ + public VoiceInteractor getVoiceInteractor() { + return mVoiceInteractor; + } + + /** * This is called for activities that set launchMode to "singleTop" in * their package, or if a client used the {@link Intent#FLAG_ACTIVITY_SINGLE_TOP} * flag when calling {@link #startActivity}. In either case, when the @@ -1347,6 +1383,7 @@ public class Activity extends ContextThemeWrapper * @see #onSaveInstanceState * @see #onPause */ + @Nullable public CharSequence onCreateDescription() { return null; } @@ -1386,6 +1423,10 @@ public class Activity extends ContextThemeWrapper protected void onStop() { if (DEBUG_LIFECYCLE) Slog.v(TAG, "onStop " + this); if (mActionBar != null) mActionBar.setShowHideAnimationEnabled(false); + if (mCalledActivityOptions != null) { + mCalledActivityOptions.dispatchActivityStopped(); + mCalledActivityOptions = null; + } getApplication().dispatchActivityStopped(this); mTranslucentCallback = null; mCalled = true; @@ -1551,6 +1592,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 +1672,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 +1685,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 +1933,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 +1978,7 @@ public class Activity extends ContextThemeWrapper return; } - mActionBar = new ActionBarImpl(this); + mActionBar = new WindowDecorActionBar(this); mActionBar.setDefaultDisplayHomeAsUpEnabled(mEnableDefaultActionBarUp); mWindow.setDefaultIcon(mActivityInfo.getIconResource()); @@ -1927,7 +1996,7 @@ public class Activity extends ContextThemeWrapper */ public void setContentView(int layoutResID) { getWindow().setContentView(layoutResID); - initActionBar(); + initWindowDecorActionBar(); } /** @@ -1947,7 +2016,7 @@ public class Activity extends ContextThemeWrapper */ public void setContentView(View view) { getWindow().setContentView(view); - initActionBar(); + initWindowDecorActionBar(); } /** @@ -1963,7 +2032,7 @@ public class Activity extends ContextThemeWrapper */ public void setContentView(View view, ViewGroup.LayoutParams params) { getWindow().setContentView(view, params); - initActionBar(); + initWindowDecorActionBar(); } /** @@ -1975,7 +2044,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 +2089,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 +2169,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 +2327,7 @@ public class Activity extends ContextThemeWrapper */ public void onBackPressed() { if (!mFragments.popBackStackImmediate()) { - finish(); + finishWithTransition(); } } @@ -2528,6 +2642,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 +2689,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 +2770,7 @@ public class Activity extends ContextThemeWrapper break; case Window.FEATURE_ACTION_BAR: - initActionBar(); + initWindowDecorActionBar(); mActionBar.dispatchMenuVisibilityChanged(false); break; } @@ -3025,6 +3140,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 +3228,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 +3350,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 +3374,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 +3391,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 +3458,7 @@ public class Activity extends ContextThemeWrapper * Convenience for calling * {@link android.view.Window#getLayoutInflater}. */ + @NonNull public LayoutInflater getLayoutInflater() { return getWindow().getLayoutInflater(); } @@ -3348,10 +3466,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 +3505,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(mWindow, null).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 +3531,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 +3547,14 @@ 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); + activityOptions.dispatchStartExit(); + mCalledActivityOptions = activityOptions; + } if (mParent == null) { Instrumentation.ActivityResult ar = mInstrumentation.execStartActivity( @@ -3505,7 +3633,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 +3665,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 +3727,7 @@ public class Activity extends ContextThemeWrapper */ @Override public void startActivity(Intent intent) { - startActivity(intent, null); + this.startActivity(intent, null); } /** @@ -3625,7 +3753,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 +3802,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 +3821,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 +3848,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 +3876,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 +3910,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 +3960,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 +3983,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 +4013,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 +4037,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 +4063,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 +4088,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 +4121,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 +4220,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 +4243,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); @@ -4203,11 +4334,10 @@ public class Activity extends ContextThemeWrapper } /** - * Call this when your activity is done and should be closed. The - * ActivityResult is propagated back to whoever launched you via - * onActivityResult(). + * Finishes the current activity and specifies whether to remove the task associated with this + * activity. */ - public void finish() { + private void finish(boolean finishTask) { if (mParent == null) { int resultCode; Intent resultData; @@ -4221,7 +4351,7 @@ public class Activity extends ContextThemeWrapper resultData.prepareToLeaveProcess(); } if (ActivityManagerNative.getDefault() - .finishActivity(mToken, resultCode, resultData)) { + .finishActivity(mToken, resultCode, resultData, finishTask)) { mFinished = true; } } catch (RemoteException e) { @@ -4233,6 +4363,15 @@ public class Activity extends ContextThemeWrapper } /** + * Call this when your activity is done and should be closed. The + * ActivityResult is propagated back to whoever launched you via + * onActivityResult(). + */ + public void finish() { + finish(false); + } + + /** * Finish this activity as well as all activities immediately below it * in the current task that have the same affinity. This is typically * used when an application can be launched on to another task (such as @@ -4276,6 +4415,22 @@ 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.app.ActivityOptions#makeSceneTransitionAnimation(android.view.Window, + * android.app.ActivityOptions.ActivityTransitionListener) + */ + public void finishWithTransition() { + if (mEnterTransitionCoordinator != null) { + mEnterTransitionCoordinator.startExit(); + } else { + finish(); + } + } + + /** * Force finish another activity that you had previously started with * {@link #startActivityForResult}. * @@ -4305,7 +4460,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); @@ -4315,6 +4470,14 @@ public class Activity extends ContextThemeWrapper } /** + * Call this when your activity is done and should be closed and the task should be completely + * removed as a part of finishing the Activity. + */ + public void finishAndRemoveTask() { + finish(true); + } + + /** * Called when an activity you launched exits, giving you the requestCode * you started it with, the resultCode it returned, and any additional * data from it. The <var>resultCode</var> will be @@ -4366,8 +4529,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 +4557,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 +4579,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 +4651,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 +4697,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 +4739,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 +4779,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 +4845,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 +4903,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 +4939,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 +5198,7 @@ public class Activity extends ContextThemeWrapper * * @see ActionMode */ + @Nullable public ActionMode startActionMode(ActionMode.Callback callback) { return mWindow.getDecorView().startActionMode(callback); } @@ -5005,9 +5214,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 +5358,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)) { @@ -5170,6 +5381,21 @@ public class Activity extends ContextThemeWrapper } } + /** + * When {@link android.app.ActivityOptions#makeSceneTransitionAnimation(android.view.Window, + * android.app.ActivityOptions.ActivityTransitionListener)} was used to start an Activity, + * the Window will be triggered to enter with a Transition. <code>listener</code> allows + * The Activity to listen to events of the entering transition and control the mapping of + * shared elements. This requires {@link Window#FEATURE_CONTENT_TRANSITIONS}. + * + * @param listener Used to listen to events in the entering transition. + */ + public void setActivityTransitionListener(ActivityOptions.ActivityTransitionListener listener) { + if (mEnterTransitionCoordinator != null) { + mEnterTransitionCoordinator.setActivityTransitionListener(listener); + } + } + // ------------------ Internal API ------------------ final void setParent(Activity parent) { @@ -5190,6 +5416,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, 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, IVoiceInteractor voiceInteractor) { attachBaseContext(context); mFragments.attachActivity(this, mContainer, null); @@ -5204,7 +5440,7 @@ public class Activity extends ContextThemeWrapper mWindow.setUiOptions(info.uiOptions); } mUiThread = Thread.currentThread(); - + mMainThread = aThread; mInstrumentation = instr; mToken = token; @@ -5217,6 +5453,8 @@ public class Activity extends ContextThemeWrapper mParent = parent; mEmbeddedID = id; mLastNonConfigurationInstances = lastNonConfigurationInstances; + mVoiceInteractor = voiceInteractor != null + ? new VoiceInteractor(this, this, voiceInteractor, Looper.myLooper()) : null; mWindow.setWindowManager( (WindowManager)context.getSystemService(Context.WINDOW_SERVICE), @@ -5227,6 +5465,12 @@ public class Activity extends ContextThemeWrapper } mWindowManager = mWindow.getWindowManager(); mCurrentConfig = config; + if (options != null) { + ActivityOptions activityOptions = new ActivityOptions(options); + if (activityOptions.getAnimationType() == ActivityOptions.ANIM_SCENE_TRANSITION) { + mEnterTransitionCoordinator = activityOptions.createEnterActivityTransition(this); + } + } } /** @hide */ @@ -5240,7 +5484,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 +5641,7 @@ public class Activity extends ContextThemeWrapper } } } - + mStopped = true; } mResumed = false; @@ -5412,7 +5656,7 @@ public class Activity extends ContextThemeWrapper mLoaderManager.doDestroy(); } } - + /** * @hide */ @@ -5420,7 +5664,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 +5680,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..9239faf 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -76,6 +76,13 @@ public class ActivityManager { public static final String META_HOME_ALTERNATE = "android.app.home.alternate"; /** + * Result for IActivityManager.startActivity: trying to start an activity under voice + * control when that activity does not support the VOICE category. + * @hide + */ + public static final int START_NOT_VOICE_COMPATIBLE = -7; + + /** * Result for IActivityManager.startActivity: an error where the * start had to be canceled. * @hide @@ -155,6 +162,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 +516,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 +555,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 +600,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 +608,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 +980,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 @@ -1570,13 +1627,6 @@ public class ActivityManager { public int lastTrimLevel; /** - * Constant for {@link #importance}: this is a persistent process. - * Only used when reporting to process observers. - * @hide - */ - public static final int IMPORTANCE_PERSISTENT = 50; - - /** * Constant for {@link #importance}: this process is running the * foreground UI. */ @@ -1691,9 +1741,16 @@ public class ActivityManager { */ public int importanceReasonImportance; + /** + * Current process state, as per PROCESS_STATE_* constants. + * @hide + */ + public int processState; + public RunningAppProcessInfo() { importance = IMPORTANCE_FOREGROUND; importanceReasonCode = REASON_UNKNOWN; + processState = PROCESS_STATE_IMPORTANT_FOREGROUND; } public RunningAppProcessInfo(String pProcessName, int pPid, String pArr[]) { @@ -1719,6 +1776,7 @@ public class ActivityManager { dest.writeInt(importanceReasonPid); ComponentName.writeToParcel(importanceReasonComponent, dest); dest.writeInt(importanceReasonImportance); + dest.writeInt(processState); } public void readFromParcel(Parcel source) { @@ -1734,6 +1792,7 @@ public class ActivityManager { importanceReasonPid = source.readInt(); importanceReasonComponent = ComponentName.readFromParcel(source); importanceReasonImportance = source.readInt(); + processState = source.readInt(); } public static final Creator<RunningAppProcessInfo> CREATOR = @@ -1938,7 +1997,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 +2285,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..b1c37de 100644 --- a/core/java/android/app/ActivityManagerNative.java +++ b/core/java/android/app/ActivityManagerNative.java @@ -43,9 +43,11 @@ import android.os.Parcelable; import android.os.RemoteException; import android.os.ServiceManager; import android.os.StrictMode; +import android.service.voice.IVoiceInteractionSession; import android.text.TextUtils; import android.util.Log; import android.util.Singleton; +import com.android.internal.app.IVoiceInteractor; import java.util.ArrayList; import java.util.List; @@ -101,9 +103,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) { } } @@ -242,6 +244,33 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM return true; } + case START_VOICE_ACTIVITY_TRANSACTION: + { + data.enforceInterface(IActivityManager.descriptor); + String callingPackage = data.readString(); + int callingPid = data.readInt(); + int callingUid = data.readInt(); + Intent intent = Intent.CREATOR.createFromParcel(data); + String resolvedType = data.readString(); + IVoiceInteractionSession session = IVoiceInteractionSession.Stub.asInterface( + data.readStrongBinder()); + IVoiceInteractor interactor = IVoiceInteractor.Stub.asInterface( + data.readStrongBinder()); + int startFlags = data.readInt(); + String profileFile = data.readString(); + ParcelFileDescriptor profileFd = data.readInt() != 0 + ? ParcelFileDescriptor.CREATOR.createFromParcel(data) : null; + Bundle options = data.readInt() != 0 + ? Bundle.CREATOR.createFromParcel(data) : null; + int userId = data.readInt(); + int result = startVoiceActivity(callingPackage, callingPid, callingUid, + intent, resolvedType, session, interactor, startFlags, + profileFile, profileFd, options, userId); + reply.writeNoException(); + reply.writeInt(result); + return true; + } + case START_NEXT_MATCHING_ACTIVITY_TRANSACTION: { data.enforceInterface(IActivityManager.descriptor); @@ -263,7 +292,8 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM if (data.readInt() != 0) { resultData = Intent.CREATOR.createFromParcel(data); } - boolean res = finishActivity(token, resultCode, resultData); + boolean finishTask = (data.readInt() != 0); + boolean res = finishActivity(token, resultCode, resultData, finishTask); reply.writeNoException(); reply.writeInt(res ? 1 : 0); return true; @@ -654,6 +684,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 +1283,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 +1727,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 +1866,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 +2102,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); @@ -2249,6 +2352,42 @@ class ActivityManagerProxy implements IActivityManager data.recycle(); return result; } + public int startVoiceActivity(String callingPackage, int callingPid, int callingUid, + Intent intent, String resolvedType, IVoiceInteractionSession session, + IVoiceInteractor interactor, int startFlags, String profileFile, + ParcelFileDescriptor profileFd, Bundle options, int userId) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeString(callingPackage); + data.writeInt(callingPid); + data.writeInt(callingUid); + intent.writeToParcel(data, 0); + data.writeString(resolvedType); + data.writeStrongBinder(session.asBinder()); + data.writeStrongBinder(interactor.asBinder()); + data.writeInt(startFlags); + data.writeString(profileFile); + if (profileFd != null) { + data.writeInt(1); + profileFd.writeToParcel(data, Parcelable.PARCELABLE_WRITE_RETURN_VALUE); + } else { + data.writeInt(0); + } + if (options != null) { + data.writeInt(1); + options.writeToParcel(data, 0); + } else { + data.writeInt(0); + } + data.writeInt(userId); + mRemote.transact(START_VOICE_ACTIVITY_TRANSACTION, data, reply, 0); + reply.readException(); + int result = reply.readInt(); + reply.recycle(); + data.recycle(); + return result; + } public boolean startNextMatchingActivity(IBinder callingActivity, Intent intent, Bundle options) throws RemoteException { Parcel data = Parcel.obtain(); @@ -2269,7 +2408,7 @@ class ActivityManagerProxy implements IActivityManager data.recycle(); return result != 0; } - public boolean finishActivity(IBinder token, int resultCode, Intent resultData) + public boolean finishActivity(IBinder token, int resultCode, Intent resultData, boolean finishTask) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); @@ -2282,6 +2421,7 @@ class ActivityManagerProxy implements IActivityManager } else { data.writeInt(0); } + data.writeInt(finishTask ? 1 : 0); mRemote.transact(FINISH_ACTIVITY_TRANSACTION, data, reply, 0); reply.readException(); boolean res = reply.readInt() != 0; @@ -2786,6 +2926,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 +3768,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 +4385,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 +4540,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 +4870,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..a49359f 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -22,7 +22,15 @@ import android.os.Bundle; import android.os.Handler; import android.os.IRemoteCallback; import android.os.RemoteException; +import android.os.ResultReceiver; +import android.transition.Transition; +import android.util.ArrayMap; +import android.util.Pair; import android.view.View; +import android.view.Window; + +import java.util.List; +import java.util.Map; /** * Helper class for building an options Bundle that can be used with @@ -30,6 +38,8 @@ import android.view.View; * 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,14 @@ 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"; + /** @hide */ public static final int ANIM_NONE = 0; /** @hide */ @@ -100,6 +118,8 @@ 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 String mPackageName; private int mAnimationType = ANIM_NONE; @@ -111,6 +131,7 @@ public class ActivityOptions { private int mStartWidth; private int mStartHeight; private IRemoteCallback mAnimationStartedListener; + private ResultReceiver mExitReceiver; /** * Create an ActivityOptions specifying a custom animation to run when @@ -156,11 +177,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; @@ -298,7 +320,60 @@ 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. + * The position of <code>sharedElement</code> will be used as the epicenter for the + * exit Transition. The position of the shared element in the launched Activity will be the + * epicenter of its entering Transition. + * + * <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 window The window containing shared elements. + * @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. + * @see android.transition.Transition#setEpicenterCallback( + * android.transition.Transition.EpicenterCallback) + */ + public static ActivityOptions makeSceneTransitionAnimation(Window window, + View sharedElement, String sharedElementName) { + return makeSceneTransitionAnimation(window, + new SharedElementMappingListener(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. The position of the first element in the value returned from + * {@link android.app.ActivityOptions.ActivityTransitionListener#getSharedElementsMapping()} + * will be used as the epicenter for the exit Transition. The position of the associated + * shared element in the launched Activity will be the epicenter of its entering Transition. + * + * <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 window The window containing shared elements. + * @param listener The listener to use to monitor activity transition events. + * @return Returns a new ActivityOptions object that you can use to + * supply these options as the options Bundle when starting an activity. + * @see android.transition.Transition#setEpicenterCallback( + * android.transition.Transition.EpicenterCallback) + */ + public static ActivityOptions makeSceneTransitionAnimation(Window window, + ActivityTransitionListener listener) { + ActivityOptions opts = new ActivityOptions(); + opts.mAnimationType = ANIM_SCENE_TRANSITION; + ExitTransitionCoordinator exit = new ExitTransitionCoordinator(window, listener); + opts.mExitReceiver = exit; return opts; } @@ -309,23 +384,33 @@ 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: + mExitReceiver = opts.getParcelable(KEY_TRANSITION_COMPLETE_LISTENER); + break; } } @@ -380,6 +465,20 @@ public class ActivityOptions { } /** @hide */ + public void dispatchActivityStopped() { + if (mExitReceiver != null) { + mExitReceiver.send(ActivityTransitionCoordinator.MSG_ACTIVITY_STOPPED, null); + } + } + + /** @hide */ + public void dispatchStartExit() { + if (mExitReceiver != null) { + mExitReceiver.send(ActivityTransitionCoordinator.MSG_START_EXIT_TRANSITION, null); + } + } + + /** @hide */ public void abort() { if (mAnimationStartedListener != null) { try { @@ -396,6 +495,15 @@ public class ActivityOptions { } } + /** @hide */ + public EnterTransitionCoordinator createEnterActivityTransition(Activity activity) { + EnterTransitionCoordinator coordinator = null; + if (mAnimationType == ANIM_SCENE_TRANSITION) { + coordinator = new EnterTransitionCoordinator(activity, mExitReceiver); + } + return coordinator; + } + /** * Update the current values in this ActivityOptions from those supplied * in <var>otherOptions</var>. Any values @@ -405,15 +513,16 @@ public class ActivityOptions { if (otherOptions.mPackageName != null) { mPackageName = otherOptions.mPackageName; } + mExitReceiver = 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) { } } @@ -425,9 +534,9 @@ 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) { } } @@ -439,14 +548,20 @@ 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; break; + case ANIM_SCENE_TRANSITION: + mAnimationType = otherOptions.mAnimationType; + mExitReceiver = otherOptions.mExitReceiver; + mThumbnail = null; + mAnimationStartedListener = null; + break; } } @@ -468,7 +583,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 +599,153 @@ 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 (mExitReceiver != null) { + b.putParcelable(KEY_TRANSITION_COMPLETE_LISTENER, mExitReceiver); + } + 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; + } + + /** + * Listener provided in + * {@link android.app.ActivityOptions#makeSceneTransitionAnimation(android.view.Window, + * android.app.ActivityOptions.ActivityTransitionListener)} or in + * {@link android.app.Activity#setActivityTransitionListener( + * android.app.ActivityOptions.ActivityTransitionListener)} to monitor the Activity transitions. + * The events can be used to customize or override Activity Transition behavior. + */ + public static class ActivityTransitionListener { + /** + * Called when the enter Transition is ready to start, but hasn't started yet. If + * {@link android.view.Window#getEnterTransition()} is non-null, + * The entering views will be {@link View#INVISIBLE}. + */ + public void onEnterReady() {} + + /** + * Called when the remote exiting transition completes. + */ + public void onRemoteExitComplete() {} + + /** + * Called when the start state for shared elements is captured on enter. + * + * @param sharedElementNames The names of the shared elements that were accepted into + * the View hierarchy. + * @param sharedElements The shared elements that are part of the View hierarchy. + * @param sharedElementSnapshots The Views containing snap shots of the shared element + * from the launching Window. These elements will not + * be part of the scene, but will be positioned relative + * to the Window decor View. + */ + public void onCaptureSharedElementStart(List<String> sharedElementNames, + List<View> sharedElements, List<View> sharedElementSnapshots) {} + + /** + * Called when the end state for shared elements is captured on enter. + * + * @param sharedElementNames The names of the shared elements that were accepted into + * the View hierarchy. + * @param sharedElements The shared elements that are part of the View hierarchy. + * @param sharedElementSnapshots The Views containing snap shots of the shared element + * from the launching Window. These elements will not + * be part of the scene, but will be positioned relative + * to the Window decor View. + */ + public void onCaptureSharedElementEnd(List<String> sharedElementNames, + List<View> sharedElements, List<View> sharedElementSnapshots) {} + + /** + * Called when the enter Transition has been started. + * @param sharedElementNames The names of shared elements that were transferred. + * @param sharedElements The shared elements that were transferred. + */ + public void onStartEnterTransition(List<String> sharedElementNames, + List<View> sharedElements) {} + + /** + * Called when the exit Transition has been started. + * @param sharedElementNames The names of all shared elements that will be transferred. + * @param sharedElements All shared elements that will be transferred. + */ + public void onStartExitTransition(List<String> sharedElementNames, + List<View> sharedElements) {} + + /** + * Called when the exiting shared element transition completes. + */ + public void onSharedElementExitTransitionComplete() {} + + /** + * Called on exit when the shared element has been transferred. + * @param sharedElementNames The names of all shared elements that were transferred. + * @param sharedElements All shared elements that will were transferred. + */ + public void onSharedElementTransferred(List<String> sharedElementNames, + List<View> sharedElements) {} + + /** + * Called when the exit transition has completed. + */ + public void onExitTransitionComplete() {} + + /** + * Returns a mapping from a View in the View hierarchy to the shared element name used + * in the call. This is called twice -- once when the view is + * entering and again when it exits. A null return value indicates that the + * View hierachy can be trusted without any remapping. + * @return A map from a View in the hierarchy to the shared element name used in the + * call. + */ + public Pair<View, String>[] getSharedElementsMapping() { return null; } + + /** + * Returns <code>true</code> if the ActivityTransitionListener will handle removing + * rejected shared elements from the scene. If <code>false</code> is returned, a default + * animation will be used to remove the rejected shared elements from the scene. + * + * @param rejectedSharedElements Views containing visual information of shared elements + * that are not part of the entering scene. These Views + * are positioned relative to the Window decor View. + * @return <code>false</code> if the default animation should be used to remove the + * rejected shared elements from the scene or <code>true</code> if the listener provides + * custom handling. + */ + public boolean handleRejectedSharedElements(List<View> rejectedSharedElements) { + return false; + } + } + + private static class SharedElementMappingListener extends ActivityTransitionListener { + Pair<View, String>[] mSharedElementsMapping = new Pair[1]; + + public SharedElementMappingListener(View view, String name) { + mSharedElementsMapping[0] = Pair.create(view, name); + } + + @Override + public Pair<View, String>[] getSharedElementsMapping() { + return mSharedElementsMapping; + } + } } diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 7f8dbba..7dc21b4 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; @@ -90,6 +94,7 @@ import android.view.WindowManagerGlobal; import android.renderscript.RenderScript; import android.security.AndroidKeyStoreProvider; +import com.android.internal.app.IVoiceInteractor; import com.android.internal.os.BinderInternal; import com.android.internal.os.RuntimeInit; import com.android.internal.os.SamplingProfilerIntegration; @@ -261,6 +266,7 @@ public final class ActivityThread { IBinder token; int ident; Intent intent; + IVoiceInteractor voiceInteractor; Bundle state; Activity activity; Window window; @@ -289,6 +295,7 @@ public final class ActivityThread { boolean isForward; int pendingConfigChanges; boolean onlyLocalRequest; + Bundle activityOptions; View mPendingRemoveWindow; WindowManager mPendingRemoveWindowManager; @@ -581,9 +588,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) { @@ -597,9 +605,11 @@ public final class ActivityThread { // activity itself back to the activity manager. (matters more with ipc) public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident, ActivityInfo info, Configuration curConfig, CompatibilityInfo compatInfo, + IVoiceInteractor voiceInteractor, 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); @@ -608,6 +618,7 @@ public final class ActivityThread { r.token = token; r.ident = ident; r.intent = intent; + r.voiceInteractor = voiceInteractor; r.activityInfo = info; r.compatInfo = compatInfo; r.state = state; @@ -621,6 +632,7 @@ public final class ActivityThread { r.profileFile = profileName; r.profileFd = profileFd; r.autoStopProfiler = autoStopProfiler; + r.activityOptions = resumeArgs; updatePendingConfiguration(curConfig); @@ -1244,7 +1256,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 +1302,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 +1596,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 +2089,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 +2142,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 +2201,8 @@ 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, + r.voiceInteractor); if (customIntent != null) { activity.mIntent = customIntent; @@ -2297,12 +2312,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) { @@ -2352,7 +2368,7 @@ public final class ActivityThread { // manager to stop us. try { ActivityManagerNative.getDefault() - .finishActivity(r.token, Activity.RESULT_CANCELED, null); + .finishActivity(r.token, Activity.RESULT_CANCELED, null, false); } catch (RemoteException ex) { // Ignore } @@ -2861,12 +2877,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) { @@ -2972,7 +2989,7 @@ public final class ActivityThread { // just end this activity. try { ActivityManagerNative.getDefault() - .finishActivity(token, Activity.RESULT_CANCELED, null); + .finishActivity(token, Activity.RESULT_CANCELED, null, false); } catch (RemoteException ex) { } } @@ -2991,11 +3008,19 @@ 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); + Configuration config = res.getConfiguration(); + boolean useAlternateRecents = (config.smallestScreenWidthDp < 600); + if (useAlternateRecents) { + 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 +3812,7 @@ public final class ActivityThread { } } r.startsNotResumed = tmp.startsNotResumed; + r.activityOptions = null; handleLaunchActivity(r, currentIntent); } @@ -3964,6 +3990,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/ActivityTransitionCoordinator.java b/core/java/android/app/ActivityTransitionCoordinator.java new file mode 100644 index 0000000..3c1455b --- /dev/null +++ b/core/java/android/app/ActivityTransitionCoordinator.java @@ -0,0 +1,842 @@ +/* + * 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.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.Handler; +import android.os.ResultReceiver; +import android.transition.Transition; +import android.transition.TransitionManager; +import android.transition.TransitionSet; +import android.util.ArrayMap; +import android.util.Pair; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroupOverlay; +import android.view.ViewTreeObserver; +import android.view.Window; +import android.widget.ImageView; + +import java.util.ArrayList; +import java.util.Collection; + +/** + * Base class for ExitTransitionCoordinator and EnterTransitionCoordinator, classes + * that manage activity transitions and the communications coordinating them between + * Activities. The ExitTransitionCoordinator is created in the + * ActivityOptions#makeSceneTransitionAnimation. The EnterTransitionCoordinator + * is created by ActivityOptions#createEnterActivityTransition by Activity when the window is + * attached. + * + * Typical startActivity goes like this: + * 1) ExitTransitionCoordinator created with ActivityOptions#makeSceneTransitionAnimation + * 2) Activity#startActivity called and that calls startExit() through + * ActivityOptions#dispatchStartExit + * - Exit transition starts by setting transitioning Views to INVISIBLE + * 3) Launched Activity starts, creating an EnterTransitionCoordinator. + * - The Window is made translucent + * - The Window background alpha is set to 0 + * - The transitioning views are made INVISIBLE + * - MSG_SET_LISTENER is sent back to the ExitTransitionCoordinator. + * 4) The shared element transition completes. + * - MSG_TAKE_SHARED_ELEMENTS is sent to the EnterTransitionCoordinator + * 5) The MSG_TAKE_SHARED_ELEMENTS is received by the EnterTransitionCoordinator. + * - Shared elements are made VISIBLE + * - Shared elements positions and size are set to match the end state of the calling + * Activity. + * - The shared element transition is started + * - If the window allows overlapping transitions, the views transition is started by setting + * the entering Views to VISIBLE and the background alpha is animated to opaque. + * - MSG_HIDE_SHARED_ELEMENTS is sent to the ExitTransitionCoordinator + * 6) MSG_HIDE_SHARED_ELEMENTS is received by the ExitTransitionCoordinator + * - The shared elements are made INVISIBLE + * 7) The exit transition completes in the calling Activity. + * - MSG_EXIT_TRANSITION_COMPLETE is sent to the EnterTransitionCoordinator. + * 8) The MSG_EXIT_TRANSITION_COMPLETE is received by the EnterTransitionCoordinator. + * - If the window doesn't allow overlapping enter transitions, the enter transition is started + * by setting entering views to VISIBLE and the background is animated to opaque. + * 9) The background opacity animation completes. + * - The window is made opaque + * 10) The calling Activity gets an onStop() call + * - onActivityStopped() is called and all exited Views are made VISIBLE. + * + * Typical finishWithTransition goes like this: + * 1) finishWithTransition() calls startExit() + * - The Window start transitioning to Translucent + * - If no background exists, a black background is substituted + * - MSG_PREPARE_RESTORE is sent to the ExitTransitionCoordinator + * - The shared elements in the scene are matched against those shared elements + * that were sent by comparing the names. + * - The exit transition is started by setting Views to INVISIBLE. + * 2) MSG_PREPARE_RESTORE is received by the EnterTransitionCoordinator + * - All transitioning views are made VISIBLE to reverse what was done when onActivityStopped() + * was called + * 3) The Window is made translucent and a callback is received + * - The background alpha is animated to 0 + * 4) The background alpha animation completes + * 5) The shared element transition completes + * - After both 4 & 5 complete, MSG_TAKE_SHARED_ELEMENTS is sent to the + * ExitTransitionCoordinator + * 6) MSG_TAKE_SHARED_ELEMENTS is received by ExitTransitionCoordinator + * - Shared elements are made VISIBLE + * - Shared elements positions and size are set to match the end state of the calling + * Activity. + * - The shared element transition is started + * - If the window allows overlapping transitions, the views transition is started by setting + * the entering Views to VISIBLE. + * - MSG_HIDE_SHARED_ELEMENTS is sent to the EnterTransitionCoordinator + * 7) MSG_HIDE_SHARED_ELEMENTS is received by the EnterTransitionCoordinator + * - The shared elements are made INVISIBLE + * 8) The exit transition completes in the finishing Activity. + * - MSG_EXIT_TRANSITION_COMPLETE is sent to the ExitTransitionCoordinator. + * - finish() is called on the exiting Activity + * 9) The MSG_EXIT_TRANSITION_COMPLETE is received by the ExitTransitionCoordinator. + * - If the window doesn't allow overlapping enter transitions, the enter transition is started + * by setting entering views to VISIBLE. + */ +abstract class ActivityTransitionCoordinator extends ResultReceiver { + private static final String TAG = "ActivityTransitionCoordinator"; + + /** + * 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. + */ + public static final String KEY_SHARED_ELEMENT_NAMES = "android:shared_element_names"; + + public static final String KEY_SHARED_ELEMENT_STATE = "android:shared_element_state"; + + /** + * For Activity transitions, the called Activity's listener to receive calls + * when transitions complete. + */ + static final String KEY_TRANSITION_RESULTS_RECEIVER = "android:transitionTargetListener"; + + private static final String KEY_SCREEN_X = "shared_element:screenX"; + private static final String KEY_SCREEN_Y = "shared_element:screenY"; + private static final String KEY_TRANSLATION_Z = "shared_element:translationZ"; + private static final String KEY_WIDTH = "shared_element:width"; + private static final String KEY_HEIGHT = "shared_element:height"; + private static final String KEY_NAME = "shared_element:name"; + private static final String KEY_BITMAP = "shared_element:bitmap"; + + /** + * Sent by the exiting coordinator (either EnterTransitionCoordinator + * or ExitTransitionCoordinator) after the shared elements have + * become stationary (shared element transition completes). This tells + * the remote coordinator to take control of the shared elements and + * that animations may begin. The remote Activity won't start entering + * until this message is received, but may wait for + * MSG_EXIT_TRANSITION_COMPLETE if allowOverlappingTransitions() is true. + */ + public static final int MSG_SET_LISTENER = 100; + + /** + * Sent by the entering coordinator to tell the exiting coordinator + * to hide its shared elements after it has started its shared + * element transition. This is temporary until the + * interlock of shared elements is figured out. + */ + public static final int MSG_HIDE_SHARED_ELEMENTS = 101; + + /** + * Sent by the EnterTransitionCoordinator to tell the + * ExitTransitionCoordinator to hide all of its exited views after + * MSG_ACTIVITY_STOPPED has caused them all to show. + */ + public static final int MSG_PREPARE_RESTORE = 102; + + /** + * Sent by the exiting Activity in ActivityOptions#dispatchActivityStopped + * to leave the Activity in a good state after it has been hidden. + */ + public static final int MSG_ACTIVITY_STOPPED = 103; + + /** + * Sent by the exiting coordinator (either EnterTransitionCoordinator + * or ExitTransitionCoordinator) after the shared elements have + * become stationary (shared element transition completes). This tells + * the remote coordinator to take control of the shared elements and + * that animations may begin. The remote Activity won't start entering + * until this message is received, but may wait for + * MSG_EXIT_TRANSITION_COMPLETE if allowOverlappingTransitions() is true. + */ + public static final int MSG_TAKE_SHARED_ELEMENTS = 104; + + /** + * Sent by the exiting coordinator (either + * EnterTransitionCoordinator or ExitTransitionCoordinator) after + * the exiting Views have finished leaving the scene. This will + * be ignored if allowOverlappingTransitions() is true on the + * remote coordinator. If it is false, it will trigger the enter + * transition to start. + */ + public static final int MSG_EXIT_TRANSITION_COMPLETE = 105; + + /** + * Sent by Activity#startActivity to begin the exit transition. + */ + public static final int MSG_START_EXIT_TRANSITION = 106; + + private Window mWindow; + private ArrayList<View> mSharedElements = new ArrayList<View>(); + private ArrayList<String> mTargetSharedNames = new ArrayList<String>(); + private ActivityOptions.ActivityTransitionListener mListener = + new ActivityOptions.ActivityTransitionListener(); + private ArrayList<View> mEnteringViews; + private ResultReceiver mRemoteResultReceiver; + private boolean mNotifiedSharedElementTransitionComplete; + private boolean mNotifiedExitTransitionComplete; + private boolean mSharedElementTransitionStarted; + + private FixedEpicenterCallback mEpicenterCallback = new FixedEpicenterCallback(); + + private Transition.TransitionListener mSharedElementListener = + new Transition.TransitionListenerAdapter() { + @Override + public void onTransitionEnd(Transition transition) { + transition.removeListener(this); + onSharedElementTransitionEnd(); + } + }; + + private Transition.TransitionListener mExitListener = + new Transition.TransitionListenerAdapter() { + @Override + public void onTransitionEnd(Transition transition) { + transition.removeListener(this); + onExitTransitionEnd(); + } + }; + + public ActivityTransitionCoordinator(Window window) + { + super(new Handler()); + mWindow = window; + } + + // -------------------- ResultsReceiver Overrides ---------------------- + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + switch (resultCode) { + case MSG_SET_LISTENER: + ResultReceiver resultReceiver + = resultData.getParcelable(KEY_TRANSITION_RESULTS_RECEIVER); + setRemoteResultReceiver(resultReceiver); + onSetResultReceiver(); + break; + case MSG_HIDE_SHARED_ELEMENTS: + onHideSharedElements(); + break; + case MSG_PREPARE_RESTORE: + onPrepareRestore(); + break; + case MSG_EXIT_TRANSITION_COMPLETE: + if (!mSharedElementTransitionStarted) { + send(resultCode, resultData); + } else { + onRemoteSceneExitComplete(); + } + break; + case MSG_TAKE_SHARED_ELEMENTS: + ArrayList<String> sharedElementNames + = resultData.getStringArrayList(KEY_SHARED_ELEMENT_NAMES); + Bundle sharedElementState = resultData.getBundle(KEY_SHARED_ELEMENT_STATE); + onTakeSharedElements(sharedElementNames, sharedElementState); + break; + case MSG_ACTIVITY_STOPPED: + onActivityStopped(); + break; + case MSG_START_EXIT_TRANSITION: + startExit(); + break; + } + } + + // -------------------- calls that can be overridden by subclasses -------------------- + + /** + * Called when MSG_SET_LISTENER is received. This will only be received by + * ExitTransitionCoordinator. + */ + protected void onSetResultReceiver() {} + + /** + * Called when MSG_HIDE_SHARED_ELEMENTS is received + */ + protected void onHideSharedElements() { + setViewVisibility(getSharedElements(), View.INVISIBLE); + mListener.onSharedElementTransferred(getSharedElementNames(), getSharedElements()); + } + + /** + * Called when MSG_PREPARE_RESTORE is called. This will only be received by + * ExitTransitionCoordinator. + */ + protected void onPrepareRestore() { + mListener.onEnterReady(); + } + + /** + * Called when MSG_EXIT_TRANSITION_COMPLETE is received -- the remote coordinator has + * completed its exit transition. This can be called by the ExitTransitionCoordinator when + * starting an Activity or EnterTransitionCoordinator when called with finishWithTransition. + */ + protected void onRemoteSceneExitComplete() { + if (!allowOverlappingTransitions()) { + Transition transition = beginTransition(mEnteringViews, false, true, true); + onStartEnterTransition(transition, mEnteringViews); + } + mListener.onRemoteExitComplete(); + } + + /** + * Called when MSG_TAKE_SHARED_ELEMENTS is received. This means that the shared elements are + * in a stable state and ready to move to the Window. + * @param sharedElementNames The names of the shared elements to move. + * @param state Contains the shared element states (size & position) + */ + protected void onTakeSharedElements(ArrayList<String> sharedElementNames, Bundle state) { + setSharedElements(); + reconcileSharedElements(sharedElementNames); + mEnteringViews.removeAll(mSharedElements); + final ArrayList<View> accepted = new ArrayList<View>(); + final ArrayList<View> rejected = new ArrayList<View>(); + createSharedElementImages(accepted, rejected, sharedElementNames, state); + setSharedElementState(state, accepted); + handleRejected(rejected); + + if (getViewsTransition() != null) { + setViewVisibility(mEnteringViews, View.INVISIBLE); + } + setViewVisibility(mSharedElements, View.VISIBLE); + Transition transition = beginTransition(mEnteringViews, true, allowOverlappingTransitions(), + true); + + if (allowOverlappingTransitions()) { + onStartEnterTransition(transition, mEnteringViews); + } + + mRemoteResultReceiver.send(MSG_HIDE_SHARED_ELEMENTS, null); + } + + /** + * Called when MSG_ACTIVITY_STOPPED is received. This is received when Activity.onStop is + * called after running startActivity* is called using an Activity Transition. + */ + protected void onActivityStopped() {} + + /** + * Called when the start transition is ready to run. This may be immediately after + * MSG_TAKE_SHARED_ELEMENTS or MSG_EXIT_TRANSITION_COMPLETE, depending on whether + * overlapping transitions are allowed. + * @param transition The transition currently started. + * @param enteringViews The views entering the scene. This won't include shared elements. + */ + protected void onStartEnterTransition(Transition transition, ArrayList<View> enteringViews) { + if (getViewsTransition() != null) { + setViewVisibility(enteringViews, View.VISIBLE); + } + mEnteringViews = null; + mListener.onStartEnterTransition(getSharedElementNames(), getSharedElements()); + } + + /** + * Called when the exit transition has started. + * @param exitingViews The views leaving the scene. This won't include shared elements. + */ + protected void onStartExitTransition(ArrayList<View> exitingViews) {} + + /** + * Called during the exit when the shared element transition has completed. + */ + protected void onSharedElementTransitionEnd() { + Bundle bundle = new Bundle(); + int[] tempLoc = new int[2]; + for (int i = 0; i < mSharedElements.size(); i++) { + View sharedElement = mSharedElements.get(i); + String name = mTargetSharedNames.get(i); + captureSharedElementState(sharedElement, name, bundle, tempLoc); + } + Bundle allValues = new Bundle(); + allValues.putStringArrayList(KEY_SHARED_ELEMENT_NAMES, getSharedElementNames()); + allValues.putBundle(KEY_SHARED_ELEMENT_STATE, bundle); + sharedElementTransitionComplete(allValues); + mListener.onSharedElementExitTransitionComplete(); + } + + /** + * Called after the shared element transition is complete to pass the shared element state + * to the remote coordinator. + * @param bundle The Bundle to send to the coordinator containing the shared element state. + */ + protected abstract void sharedElementTransitionComplete(Bundle bundle); + + /** + * Called when the exit transition finishes. + */ + protected void onExitTransitionEnd() { + mListener.onExitTransitionComplete(); + } + + /** + * Called to start the exit transition. Launched from ActivityOptions#dispatchStartExit + */ + protected abstract void startExit(); + + /** + * A non-null transition indicates that the Views of the Window should be made INVISIBLE. + * @return The Transition used to cause transitioning views to either enter or exit the scene. + */ + protected abstract Transition getViewsTransition(); + + /** + * @return The Transition used to move the shared elements from the start position and size + * to the end position and size. + */ + protected abstract Transition getSharedElementTransition(); + + /** + * @return When the enter transition should overlap with the exit transition of the + * remote controller. + */ + protected abstract boolean allowOverlappingTransitions(); + + // called by subclasses + + protected void notifySharedElementTransitionComplete(Bundle sharedElements) { + if (!mNotifiedSharedElementTransitionComplete) { + mNotifiedSharedElementTransitionComplete = true; + mRemoteResultReceiver.send(MSG_TAKE_SHARED_ELEMENTS, sharedElements); + } + } + + protected void notifyExitTransitionComplete() { + if (!mNotifiedExitTransitionComplete) { + mNotifiedExitTransitionComplete = true; + mRemoteResultReceiver.send(MSG_EXIT_TRANSITION_COMPLETE, null); + } + } + + protected void notifyPrepareRestore() { + mRemoteResultReceiver.send(MSG_PREPARE_RESTORE, null); + } + + protected void setRemoteResultReceiver(ResultReceiver resultReceiver) { + mRemoteResultReceiver = resultReceiver; + } + + protected void notifySetListener() { + Bundle bundle = new Bundle(); + bundle.putParcelable(KEY_TRANSITION_RESULTS_RECEIVER, this); + mRemoteResultReceiver.send(MSG_SET_LISTENER, bundle); + } + + protected void setEnteringViews(ArrayList<View> views) { + mEnteringViews = views; + } + + protected void setSharedElements() { + Pair<View, String>[] sharedElements = mListener.getSharedElementsMapping(); + mSharedElements.clear(); + mTargetSharedNames.clear(); + if (sharedElements == null) { + ArrayMap<String, View> map = new ArrayMap<String, View>(); + if (getViewsTransition() != null) { + setViewVisibility(mEnteringViews, View.VISIBLE); + } + getDecor().findSharedElements(map); + if (getViewsTransition() != null) { + setViewVisibility(mEnteringViews, View.INVISIBLE); + } + for (int i = 0; i < map.size(); i++) { + View view = map.valueAt(i); + String name = map.keyAt(i); + mSharedElements.add(view); + mTargetSharedNames.add(name); + } + } else { + for (int i = 0; i < sharedElements.length; i++) { + Pair<View, String> viewStringPair = sharedElements[i]; + View view = viewStringPair.first; + String name = viewStringPair.second; + mSharedElements.add(view); + mTargetSharedNames.add(name); + } + } + } + + protected ArrayList<View> getSharedElements() { + return mSharedElements; + } + + protected ArrayList<String> getSharedElementNames() { + return mTargetSharedNames; + } + + protected Window getWindow() { + return mWindow; + } + + protected ViewGroup getDecor() { + return (mWindow == null) ? null : (ViewGroup) mWindow.getDecorView(); + } + + protected void startExitTransition(ArrayList<String> sharedElements) { + setSharedElements(); + reconcileSharedElements(sharedElements); + ArrayList<View> transitioningViews = captureTransitioningViews(); + beginTransition(transitioningViews, true, true, false); + onStartExitTransition(transitioningViews); + if (getViewsTransition() != null) { + setViewVisibility(transitioningViews, View.INVISIBLE); + } + mListener.onStartExitTransition(getSharedElementNames(), getSharedElements()); + } + + protected void clearConnections() { + mRemoteResultReceiver = null; + } + + // public API + + public void setActivityTransitionListener(ActivityOptions.ActivityTransitionListener listener) { + if (listener == null) { + mListener = new ActivityOptions.ActivityTransitionListener(); + } else { + mListener = listener; + } + } + + // private methods + + private Transition configureTransition(Transition transition) { + if (transition != null) { + transition = transition.clone(); + transition.setEpicenterCallback(mEpicenterCallback); + } + return transition; + } + + private void reconcileSharedElements(ArrayList<String> sharedElementNames) { + // keep only those that are in sharedElementNames. + int numSharedElements = sharedElementNames.size(); + int targetIndex = 0; + for (int i = 0; i < numSharedElements; i++) { + String name = sharedElementNames.get(i); + int index = mTargetSharedNames.indexOf(name); + if (index >= 0) { + // Swap the items at the indexes if necessary. + if (index != targetIndex) { + View temp = mSharedElements.get(index); + mSharedElements.set(index, mSharedElements.get(targetIndex)); + mSharedElements.set(targetIndex, temp); + mTargetSharedNames.set(index, mTargetSharedNames.get(targetIndex)); + mTargetSharedNames.set(targetIndex, name); + } + targetIndex++; + } + } + for (int i = mSharedElements.size() - 1; i >= targetIndex; i--) { + mSharedElements.remove(i); + mTargetSharedNames.remove(i); + } + Rect epicenter = null; + if (!mTargetSharedNames.isEmpty() + && mTargetSharedNames.get(0).equals(sharedElementNames.get(0))) { + epicenter = calcEpicenter(mSharedElements.get(0)); + } + mEpicenterCallback.setEpicenter(epicenter); + } + + private void setSharedElementState(Bundle sharedElementState, + final ArrayList<View> acceptedOverlayViews) { + final int[] tempLoc = new int[2]; + if (sharedElementState != null) { + for (int i = 0; i < mSharedElements.size(); i++) { + View sharedElement = mSharedElements.get(i); + View parent = (View) sharedElement.getParent(); + parent.getLocationOnScreen(tempLoc); + String name = mTargetSharedNames.get(i); + setSharedElementState(sharedElement, name, sharedElementState, tempLoc); + sharedElement.requestLayout(); + } + } + mListener.onCaptureSharedElementStart(mTargetSharedNames, mSharedElements, + acceptedOverlayViews); + + getDecor().getViewTreeObserver().addOnPreDrawListener( + new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + getDecor().getViewTreeObserver().removeOnPreDrawListener(this); + mListener.onCaptureSharedElementEnd(mTargetSharedNames, mSharedElements, + acceptedOverlayViews); + mSharedElementTransitionStarted = true; + return true; + } + } + ); + } + + /** + * Sets the captured values from a previous + * {@link #captureSharedElementState(android.view.View, String, android.os.Bundle, int[])} + * @param view The View to apply placement changes to. + * @param name The shared element name given from the source Activity. + * @param transitionArgs A <code>Bundle</code> containing all placementinformation for named + * shared elements in the scene. + * @param parentLoc The x and y coordinates of the parent's screen position. + */ + private static void setSharedElementState(View view, String name, Bundle transitionArgs, + int[] parentLoc) { + Bundle sharedElementBundle = transitionArgs.getBundle(name); + if (sharedElementBundle == null) { + return; + } + + float z = sharedElementBundle.getFloat(KEY_TRANSLATION_Z); + view.setTranslationZ(z); + + int x = sharedElementBundle.getInt(KEY_SCREEN_X); + int y = sharedElementBundle.getInt(KEY_SCREEN_Y); + int width = sharedElementBundle.getInt(KEY_WIDTH); + int height = sharedElementBundle.getInt(KEY_HEIGHT); + + int widthSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY); + int heightSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY); + view.measure(widthSpec, heightSpec); + + int left = x - parentLoc[0]; + int top = y - parentLoc[1]; + int right = left + width; + int bottom = top + height; + view.layout(left, top, right, bottom); + } + + /** + * Captures placement information for Views with a shared element name for + * Activity Transitions. + * @param view The View to capture the placement information for. + * @param name The shared element name in the target Activity to apply the placement + * information for. + * @param transitionArgs Bundle to store shared element placement information. + * @param tempLoc A temporary int[2] for capturing the current location of views. + * @see #setSharedElementState(android.view.View, String, android.os.Bundle, int[]) + */ + private static void captureSharedElementState(View view, String name, Bundle transitionArgs, + int[] tempLoc) { + Bundle sharedElementBundle = new Bundle(); + view.getLocationOnScreen(tempLoc); + float scaleX = view.getScaleX(); + sharedElementBundle.putInt(KEY_SCREEN_X, tempLoc[0]); + int width = Math.round(view.getWidth() * scaleX); + sharedElementBundle.putInt(KEY_WIDTH, width); + + float scaleY = view.getScaleY(); + sharedElementBundle.putInt(KEY_SCREEN_Y, tempLoc[1]); + int height= Math.round(view.getHeight() * scaleY); + sharedElementBundle.putInt(KEY_HEIGHT, height); + + sharedElementBundle.putFloat(KEY_TRANSLATION_Z, view.getTranslationZ()); + + sharedElementBundle.putString(KEY_NAME, view.getSharedElementName()); + + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + view.draw(canvas); + sharedElementBundle.putParcelable(KEY_BITMAP, bitmap); + + transitionArgs.putBundle(name, sharedElementBundle); + } + + private static Rect calcEpicenter(View view) { + int[] loc = new int[2]; + view.getLocationOnScreen(loc); + int left = loc[0] + Math.round(view.getTranslationX()); + int top = loc[1] + Math.round(view.getTranslationY()); + int right = left + view.getWidth(); + int bottom = top + view.getHeight(); + return new Rect(left, top, right, bottom); + } + + public static void setViewVisibility(Collection<View> views, int visibility) { + if (views != null) { + for (View view : views) { + view.setVisibility(visibility); + } + } + } + + private static Transition addTransitionTargets(Transition transition, Collection<View> views) { + if (transition == null || views == null || views.isEmpty()) { + return null; + } + TransitionSet set = new TransitionSet(); + set.addTransition(transition.clone()); + if (views != null) { + for (View view: views) { + set.addTarget(view); + } + } + return set; + } + + private ArrayList<View> captureTransitioningViews() { + if (getViewsTransition() == null) { + return null; + } + ArrayList<View> transitioningViews = new ArrayList<View>(); + getDecor().captureTransitioningViews(transitioningViews); + transitioningViews.removeAll(getSharedElements()); + return transitioningViews; + } + + private Transition getSharedElementTransition(boolean isEnter) { + Transition transition = getSharedElementTransition(); + if (transition == null) { + return null; + } + transition = configureTransition(transition); + if (!isEnter) { + transition.addListener(mSharedElementListener); + } + return transition; + } + + private Transition getViewsTransition(ArrayList<View> transitioningViews, boolean isEnter) { + Transition transition = getViewsTransition(); + if (transition == null) { + return null; + } + transition = configureTransition(transition); + if (!isEnter) { + transition.addListener(mExitListener); + } + return addTransitionTargets(transition, transitioningViews); + } + + private Transition beginTransition(ArrayList<View> transitioningViews, + boolean transitionSharedElement, boolean transitionViews, boolean isEnter) { + Transition sharedElementTransition = null; + if (transitionSharedElement) { + sharedElementTransition = getSharedElementTransition(isEnter); + if (!isEnter && sharedElementTransition == null) { + onSharedElementTransitionEnd(); + } + } + Transition viewsTransition = null; + if (transitionViews) { + viewsTransition = getViewsTransition(transitioningViews, isEnter); + if (!isEnter && viewsTransition == null) { + onExitTransitionEnd(); + } + } + + Transition transition = null; + if (sharedElementTransition == null) { + transition = viewsTransition; + } else if (viewsTransition == null) { + transition = sharedElementTransition; + } else { + TransitionSet set = new TransitionSet(); + set.addTransition(sharedElementTransition); + set.addTransition(viewsTransition); + transition = set; + } + if (transition != null) { + TransitionManager.beginDelayedTransition(getDecor(), transition); + if (transitionSharedElement && !mSharedElements.isEmpty()) { + mSharedElements.get(0).invalidate(); + } else if (transitionViews && !transitioningViews.isEmpty()) { + transitioningViews.get(0).invalidate(); + } + } + return transition; + } + + private void handleRejected(final ArrayList<View> rejected) { + int numRejected = rejected.size(); + if (numRejected == 0) { + return; + } + boolean rejectionHandled = mListener.handleRejectedSharedElements(rejected); + if (rejectionHandled) { + return; + } + + ViewGroupOverlay overlay = getDecor().getOverlay(); + ObjectAnimator animator = null; + for (int i = 0; i < numRejected; i++) { + View view = rejected.get(i); + overlay.add(view); + animator = ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0); + animator.start(); + } + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + ViewGroupOverlay overlay = getDecor().getOverlay(); + for (int i = rejected.size() - 1; i >= 0; i--) { + overlay.remove(rejected.get(i)); + } + } + }); + } + + private void createSharedElementImages(ArrayList<View> accepted, ArrayList<View> rejected, + ArrayList<String> sharedElementNames, Bundle state) { + int numSharedElements = sharedElementNames.size(); + Context context = getWindow().getContext(); + int[] parentLoc = new int[2]; + getDecor().getLocationOnScreen(parentLoc); + for (int i = 0; i < numSharedElements; i++) { + String name = sharedElementNames.get(i); + Bundle sharedElementBundle = state.getBundle(name); + if (sharedElementBundle != null) { + Bitmap bitmap = sharedElementBundle.getParcelable(KEY_BITMAP); + ImageView imageView = new ImageView(context); + imageView.setId(com.android.internal.R.id.shared_element); + imageView.setScaleType(ImageView.ScaleType.CENTER); + imageView.setImageBitmap(bitmap); + imageView.setSharedElementName(name); + setSharedElementState(imageView, name, state, parentLoc); + if (mTargetSharedNames.contains(name)) { + accepted.add(imageView); + } else { + rejected.add(imageView); + } + } + } + } + + private static class FixedEpicenterCallback extends Transition.EpicenterCallback { + private Rect mEpicenter; + + public void setEpicenter(Rect epicenter) { mEpicenter = epicenter; } + + @Override + public Rect getEpicenter(Transition transition) { + return mEpicenter; + } + } +} 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..fcc7f8e 100644 --- a/core/java/android/app/ApplicationThreadNative.java +++ b/core/java/android/app/ApplicationThreadNative.java @@ -33,6 +33,7 @@ import android.os.RemoteException; import android.os.IBinder; import android.os.Parcel; import android.os.ParcelFileDescriptor; +import com.android.internal.app.IVoiceInteractor; import java.io.FileDescriptor; import java.io.IOException; @@ -113,7 +114,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; } @@ -135,6 +137,8 @@ public abstract class ApplicationThreadNative extends Binder ActivityInfo info = ActivityInfo.CREATOR.createFromParcel(data); Configuration curConfig = Configuration.CREATOR.createFromParcel(data); CompatibilityInfo compatInfo = CompatibilityInfo.CREATOR.createFromParcel(data); + IVoiceInteractor voiceInteractor = IVoiceInteractor.Stub.asInterface( + data.readStrongBinder()); int procState = data.readInt(); Bundle state = data.readBundle(); List<ResultInfo> ri = data.createTypedArrayList(ResultInfo.CREATOR); @@ -145,8 +149,11 @@ public abstract class ApplicationThreadNative extends Binder ParcelFileDescriptor profileFd = data.readInt() != 0 ? ParcelFileDescriptor.CREATOR.createFromParcel(data) : null; boolean autoStopProfiler = data.readInt() != 0; - scheduleLaunchActivity(intent, b, ident, info, curConfig, compatInfo, procState, state, - ri, pi, notResumed, isForward, profileName, profileFd, autoStopProfiler); + Bundle resumeArgs = data.readBundle(); + scheduleLaunchActivity(intent, b, ident, info, curConfig, compatInfo, + voiceInteractor, procState, state, + ri, pi, notResumed, isForward, profileName, profileFd, autoStopProfiler, + resumeArgs); return true; } @@ -705,20 +712,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); @@ -730,10 +739,12 @@ class ApplicationThreadProxy implements IApplicationThread { public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident, ActivityInfo info, Configuration curConfig, CompatibilityInfo compatInfo, + IVoiceInteractor voiceInteractor, 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); @@ -742,6 +753,7 @@ class ApplicationThreadProxy implements IApplicationThread { info.writeToParcel(data, 0); curConfig.writeToParcel(data, 0); compatInfo.writeToParcel(data, 0); + data.writeStrongBinder(voiceInteractor != null ? voiceInteractor.asBinder() : null); data.writeInt(procState); data.writeBundle(state); data.writeTypedList(pendingResults); @@ -756,6 +768,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 +899,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..a4b2651 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.SessionManager; 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); @@ -457,13 +473,14 @@ class ContextImpl extends Context { registerService(NOTIFICATION_SERVICE, new ServiceFetcher() { public Object createService(ContextImpl ctx) { final Context outerContext = ctx.getOuterContext(); + // TODO: Why are we not just using the theme attribute + // that defines the dialog theme? return new NotificationManager( new ContextThemeWrapper(outerContext, - Resources.selectSystemTheme(0, + outerContext.getResources().selectSystemTheme(0, 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.array.system_theme_sdks, + com.android.internal.R.array.system_theme_dialog_styles)), 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 SessionManager(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) { @@ -674,7 +731,7 @@ class ContextImpl extends Context { @Override public Resources.Theme getTheme() { if (mTheme == null) { - mThemeResource = Resources.selectDefaultTheme(mThemeResource, + mThemeResource = mResources.selectDefaultTheme(mThemeResource, getOuterContext().getApplicationInfo().targetSdkVersion); mTheme = mResources.newTheme(); mTheme.applyStyle(mThemeResource, true); @@ -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; } } } @@ -1808,17 +1865,26 @@ class ContextImpl extends Context { } private String uriModeFlagToString(int uriModeFlags) { - switch (uriModeFlags) { - case Intent.FLAG_GRANT_READ_URI_PERMISSION | - Intent.FLAG_GRANT_WRITE_URI_PERMISSION: - return "read and write"; - case Intent.FLAG_GRANT_READ_URI_PERMISSION: - return "read"; - case Intent.FLAG_GRANT_WRITE_URI_PERMISSION: - return "write"; + StringBuilder builder = new StringBuilder(); + if ((uriModeFlags & Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0) { + builder.append("read and "); + } + if ((uriModeFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) { + builder.append("write and "); + } + if ((uriModeFlags & Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) != 0) { + builder.append("persistable and "); + } + if ((uriModeFlags & Intent.FLAG_GRANT_PREFIX_URI_PERMISSION) != 0) { + builder.append("prefix and "); + } + + if (builder.length() > 5) { + builder.setLength(builder.length() - 5); + return builder.toString(); + } else { + throw new IllegalArgumentException("Unknown permission mode flags: " + uriModeFlags); } - throw new IllegalArgumentException( - "Unknown permission mode flags: " + uriModeFlags); } private void enforceForUri( @@ -2036,8 +2102,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/EnterTransitionCoordinator.java b/core/java/android/app/EnterTransitionCoordinator.java new file mode 100644 index 0000000..cbb8359 --- /dev/null +++ b/core/java/android/app/EnterTransitionCoordinator.java @@ -0,0 +1,287 @@ +/* + * 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.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.ResultReceiver; +import android.transition.Transition; +import android.view.View; +import android.view.ViewTreeObserver; +import android.view.Window; + +import java.util.ArrayList; + +/** + * This ActivityTransitionCoordinator is created by the Activity to manage + * the enter scene and shared element transfer as well as Activity#finishWithTransition + * exiting the Scene and transferring shared elements back to the called Activity. + */ +class EnterTransitionCoordinator extends ActivityTransitionCoordinator + implements ViewTreeObserver.OnPreDrawListener { + private static final String TAG = "EnterTransitionCoordinator"; + + // The background fade in/out duration. 150ms is pretty quick, but not abrupt. + private static final int FADE_BACKGROUND_DURATION_MS = 150; + + /** + * The shared element names sent by the ExitTransitionCoordinator and may be + * shared when exiting back. + */ + private ArrayList<String> mEnteringSharedElementNames; + + /** + * The Activity that has created this coordinator. This is used solely to make the + * Window translucent/opaque. + */ + private Activity mActivity; + + /** + * True if the Window was opaque at the start and we should make it opaque again after + * enter transitions have completed. + */ + private boolean mWasOpaque; + + /** + * During exit, is the background alpha == 0? + */ + private boolean mBackgroundFadedOut; + + /** + * During exit, has the shared element transition completed? + */ + private boolean mSharedElementTransitionComplete; + + /** + * Has the exit started? We don't want to accidentally exit multiple times. e.g. when + * back is hit twice during the exit animation. + */ + private boolean mExitTransitionStarted; + + /** + * Has the exit transition ended? + */ + private boolean mExitTransitionComplete; + + /** + * We only want to make the Window transparent and set the background alpha once. After that, + * the Activity won't want the same enter transition. + */ + private boolean mMadeReady; + + /** + * True if Window.hasFeature(Window.FEATURE_CONTENT_TRANSITIONS) -- this means that + * enter and exit transitions should be active. + */ + private boolean mSupportsTransition; + + /** + * Background alpha animations may complete prior to receiving the callback for + * onTranslucentConversionComplete. If so, we need to immediately call to make the Window + * opaque. + */ + private boolean mMakeOpaque; + + public EnterTransitionCoordinator(Activity activity, ResultReceiver resultReceiver) { + super(activity.getWindow()); + mActivity = activity; + setRemoteResultReceiver(resultReceiver); + } + + public void readyToEnter() { + if (!mMadeReady) { + mMadeReady = true; + mSupportsTransition = getWindow().hasFeature(Window.FEATURE_CONTENT_TRANSITIONS); + if (mSupportsTransition) { + Window window = getWindow(); + window.getDecorView().getViewTreeObserver().addOnPreDrawListener(this); + mActivity.overridePendingTransition(0, 0); + mActivity.convertToTranslucent(new Activity.TranslucentConversionListener() { + @Override + public void onTranslucentConversionComplete(boolean drawComplete) { + mWasOpaque = true; + if (mMakeOpaque) { + mActivity.convertFromTranslucent(); + } + } + }); + Drawable background = getDecor().getBackground(); + if (background != null) { + window.setBackgroundDrawable(null); + background.setAlpha(0); + window.setBackgroundDrawable(background); + } + } + } + } + + @Override + protected void onTakeSharedElements(ArrayList<String> sharedElementNames, Bundle state) { + mEnteringSharedElementNames = new ArrayList<String>(); + mEnteringSharedElementNames.addAll(sharedElementNames); + super.onTakeSharedElements(sharedElementNames, state); + } + + @Override + protected void sharedElementTransitionComplete(Bundle bundle) { + notifySharedElementTransitionComplete(bundle); + exitAfterSharedElementTransition(); + } + + @Override + public boolean onPreDraw() { + getWindow().getDecorView().getViewTreeObserver().removeOnPreDrawListener(this); + setEnteringViews(readyEnteringViews()); + notifySetListener(); + onPrepareRestore(); + return false; + } + + @Override + public void startExit() { + if (!mExitTransitionStarted) { + mExitTransitionStarted = true; + startExitTransition(mEnteringSharedElementNames); + } + } + + @Override + protected Transition getViewsTransition() { + if (!mSupportsTransition) { + return null; + } + return getWindow().getEnterTransition(); + } + + @Override + protected Transition getSharedElementTransition() { + if (!mSupportsTransition) { + return null; + } + return getWindow().getSharedElementEnterTransition(); + } + + @Override + protected void onStartEnterTransition(Transition transition, ArrayList<View> enteringViews) { + Drawable background = getDecor().getBackground(); + if (background != null) { + ObjectAnimator animator = ObjectAnimator.ofInt(background, "alpha", 255); + animator.setDuration(FADE_BACKGROUND_DURATION_MS); + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mMakeOpaque = true; + if (mWasOpaque) { + mActivity.convertFromTranslucent(); + } + } + }); + animator.start(); + } else if (mWasOpaque) { + transition.addListener(new Transition.TransitionListenerAdapter() { + @Override + public void onTransitionEnd(Transition transition) { + mMakeOpaque = true; + mActivity.convertFromTranslucent(); + } + }); + } + super.onStartEnterTransition(transition, enteringViews); + } + + public ArrayList<View> readyEnteringViews() { + ArrayList<View> enteringViews = new ArrayList<View>(); + getDecor().captureTransitioningViews(enteringViews); + if (getViewsTransition() != null) { + setViewVisibility(enteringViews, View.INVISIBLE); + } + return enteringViews; + } + + @Override + protected void startExitTransition(ArrayList<String> sharedElements) { + mMakeOpaque = false; + notifyPrepareRestore(); + + if (getDecor().getBackground() == null) { + ColorDrawable black = new ColorDrawable(0xFF000000); + getWindow().setBackgroundDrawable(black); + } + if (mWasOpaque) { + mActivity.convertToTranslucent(new Activity.TranslucentConversionListener() { + @Override + public void onTranslucentConversionComplete(boolean drawComplete) { + fadeOutBackground(); + } + }); + } else { + fadeOutBackground(); + } + + super.startExitTransition(sharedElements); + } + + private void fadeOutBackground() { + ObjectAnimator animator = ObjectAnimator.ofInt(getDecor().getBackground(), + "alpha", 0); + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mBackgroundFadedOut = true; + if (mSharedElementTransitionComplete) { + EnterTransitionCoordinator.super.onSharedElementTransitionEnd(); + } + } + }); + animator.setDuration(FADE_BACKGROUND_DURATION_MS); + animator.start(); + } + + @Override + protected void onExitTransitionEnd() { + mExitTransitionComplete = true; + exitAfterSharedElementTransition(); + super.onExitTransitionEnd(); + } + + @Override + protected void onSharedElementTransitionEnd() { + mSharedElementTransitionComplete = true; + if (mBackgroundFadedOut) { + super.onSharedElementTransitionEnd(); + } + } + + @Override + protected boolean allowOverlappingTransitions() { + return getWindow().getAllowEnterTransitionOverlap(); + } + + private void exitAfterSharedElementTransition() { + if (mSharedElementTransitionComplete && mExitTransitionComplete && mBackgroundFadedOut) { + mActivity.finish(); + if (mSupportsTransition) { + mActivity.overridePendingTransition(0, 0); + } + notifyExitTransitionComplete(); + clearConnections(); + } + } +} diff --git a/core/java/android/app/ExitTransitionCoordinator.java b/core/java/android/app/ExitTransitionCoordinator.java new file mode 100644 index 0000000..d920787 --- /dev/null +++ b/core/java/android/app/ExitTransitionCoordinator.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.app; + +import android.os.Bundle; +import android.transition.Transition; +import android.util.Pair; +import android.view.View; +import android.view.Window; + +import java.util.ArrayList; + +/** + * This ActivityTransitionCoordinator is created in ActivityOptions#makeSceneTransitionAnimation + * to govern the exit of the Scene and the shared elements when calling an Activity as well as + * the reentry of the Scene when coming back from the called Activity. + */ +class ExitTransitionCoordinator extends ActivityTransitionCoordinator { + private static final String TAG = "ExitTransitionCoordinator"; + + /** + * The Views that have exited and need to be restored to VISIBLE when returning to the + * normal state. + */ + private ArrayList<View> mTransitioningViews; + + /** + * Has the exit started? We don't want to accidentally exit multiple times. + */ + private boolean mExitStarted; + + /** + * Has the called Activity's ResultReceiver been set? + */ + private boolean mIsResultReceiverSet; + + /** + * Has the exit transition completed? If so, we can notify as soon as the ResultReceiver + * has been set. + */ + private boolean mExitComplete; + + /** + * Has the shared element transition completed? If so, we can notify as soon as the + * ResultReceiver has been set. + */ + private Bundle mSharedElements; + + /** + * Has the shared element transition completed? + */ + private boolean mSharedElementsComplete; + + public ExitTransitionCoordinator(Window window, + ActivityOptions.ActivityTransitionListener listener) { + super(window); + setActivityTransitionListener(listener); + } + + @Override + protected void onSetResultReceiver() { + mIsResultReceiverSet = true; + notifyCompletions(); + } + + @Override + protected void onPrepareRestore() { + makeTransitioningViewsInvisible(); + setEnteringViews(mTransitioningViews); + mTransitioningViews = null; + super.onPrepareRestore(); + } + + @Override + protected void onTakeSharedElements(ArrayList<String> sharedElementNames, Bundle state) { + super.onTakeSharedElements(sharedElementNames, state); + clearConnections(); + } + + @Override + protected void onActivityStopped() { + if (getViewsTransition() != null) { + setViewVisibility(mTransitioningViews, View.VISIBLE); + } + super.onActivityStopped(); + } + + @Override + protected void sharedElementTransitionComplete(Bundle bundle) { + mSharedElements = bundle; + mSharedElementsComplete = true; + notifyCompletions(); + } + + @Override + protected void onExitTransitionEnd() { + mExitComplete = true; + notifyCompletions(); + super.onExitTransitionEnd(); + } + + private void notifyCompletions() { + if (mIsResultReceiverSet && mSharedElementsComplete) { + if (mSharedElements != null) { + notifySharedElementTransitionComplete(mSharedElements); + mSharedElements = null; + } + if (mExitComplete) { + notifyExitTransitionComplete(); + } + } + } + + @Override + public void startExit() { + if (!mExitStarted) { + mExitStarted = true; + setSharedElements(); + startExitTransition(getSharedElementNames()); + } + } + + @Override + protected Transition getViewsTransition() { + if (!getWindow().hasFeature(Window.FEATURE_CONTENT_TRANSITIONS)) { + return null; + } + return getWindow().getExitTransition(); + } + + @Override + protected Transition getSharedElementTransition() { + if (!getWindow().hasFeature(Window.FEATURE_CONTENT_TRANSITIONS)) { + return null; + } + return getWindow().getSharedElementExitTransition(); + } + + private void makeTransitioningViewsInvisible() { + if (getViewsTransition() != null) { + setViewVisibility(mTransitioningViews, View.INVISIBLE); + } + } + + @Override + protected void onStartExitTransition(ArrayList<View> exitingViews) { + mTransitioningViews = new ArrayList<View>(); + if (exitingViews != null) { + mTransitioningViews.addAll(exitingViews); + } + mTransitioningViews.addAll(getSharedElements()); + } + + @Override + protected boolean allowOverlappingTransitions() { + return getWindow().getAllowExitTransitionOverlap(); + } +} 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..6b94c4e 100644 --- a/core/java/android/app/IActivityManager.java +++ b/core/java/android/app/IActivityManager.java @@ -47,6 +47,8 @@ import android.os.ParcelFileDescriptor; import android.os.Parcelable; import android.os.RemoteException; import android.os.StrictMode; +import android.service.voice.IVoiceInteractionSession; +import com.android.internal.app.IVoiceInteractor; import java.util.List; @@ -77,9 +79,13 @@ public interface IActivityManager extends IInterface { IntentSender intent, Intent fillInIntent, String resolvedType, IBinder resultTo, String resultWho, int requestCode, int flagsMask, int flagsValues, Bundle options) throws RemoteException; + public int startVoiceActivity(String callingPackage, int callingPid, int callingUid, + Intent intent, String resolvedType, IVoiceInteractionSession session, + IVoiceInteractor interactor, int flags, String profileFile, + ParcelFileDescriptor profileFd, Bundle options, int userId) throws RemoteException; public boolean startNextMatchingActivity(IBinder callingActivity, Intent intent, Bundle options) throws RemoteException; - public boolean finishActivity(IBinder token, int code, Intent data) + public boolean finishActivity(IBinder token, int code, Intent data, boolean finishTask) throws RemoteException; public void finishSubActivity(IBinder token, String resultWho, int requestCode) throws RemoteException; public boolean finishActivityAffinity(IBinder token) throws RemoteException; @@ -122,6 +128,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 +248,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 +352,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 +371,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 +426,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 +728,16 @@ 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; + int START_VOICE_ACTIVITY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+218; } diff --git a/core/java/android/app/IApplicationThread.java b/core/java/android/app/IApplicationThread.java index 3aceff9..f290e94 100644 --- a/core/java/android/app/IApplicationThread.java +++ b/core/java/android/app/IApplicationThread.java @@ -31,6 +31,8 @@ import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.IBinder; import android.os.IInterface; +import android.service.voice.IVoiceInteractionSession; +import com.android.internal.app.IVoiceInteractor; import java.io.FileDescriptor; import java.util.List; @@ -50,15 +52,17 @@ 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; + IVoiceInteractor voiceInteractor, int procState, Bundle state, + List<ResultInfo> pendingResults, 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..b917263 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -18,11 +18,16 @@ package android.app; import android.app.ITransientNotification; -import android.service.notification.StatusBarNotification; import android.app.Notification; import android.content.ComponentName; import android.content.Intent; +import android.net.Uri; +import android.service.notification.Condition; +import android.service.notification.IConditionListener; +import android.service.notification.IConditionProvider; import android.service.notification.INotificationListener; +import android.service.notification.StatusBarNotification; +import android.service.notification.ZenModeConfig; /** {@hide} */ interface INotificationManager @@ -31,7 +36,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); @@ -49,4 +54,12 @@ interface INotificationManager StatusBarNotification[] getActiveNotificationsFromListener(in INotificationListener token, in String[] keys); String[] getActiveNotificationKeysFromListener(in INotificationListener token); + + ZenModeConfig getZenModeConfig(); + boolean setZenModeConfig(in ZenModeConfig config); + oneway void notifyConditions(String pkg, in IConditionProvider provider, in Condition[] conditions); + oneway void requestZenModeConditions(in IConditionListener callback, int relevance); + oneway void setZenModeCondition(in Uri conditionId); + oneway void setAutomaticZenModeConditions(in Uri[] conditionIds); + Condition[] getAutomaticZenModeConditions(); }
\ No newline at end of file diff --git a/core/java/android/app/IProcessObserver.aidl b/core/java/android/app/IProcessObserver.aidl index e587912..ecf2c73 100644 --- a/core/java/android/app/IProcessObserver.aidl +++ b/core/java/android/app/IProcessObserver.aidl @@ -20,7 +20,7 @@ package android.app; oneway interface IProcessObserver { void onForegroundActivitiesChanged(int pid, int uid, boolean foregroundActivities); - void onImportanceChanged(int pid, int uid, int importance); + void onProcessStateChanged(int pid, int uid, int procState); void onProcessDied(int pid, int uid); } 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/Instrumentation.java b/core/java/android/app/Instrumentation.java index 028fa68..e58ccb8 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -1428,7 +1428,7 @@ public class Instrumentation { } /** - * Like {@link #execStartActivity(Context, IBinder, IBinder, Activity, Intent, int)}, + * Like {@link #execStartActivity}, * but accepts an array of activities to be started. Note that active * {@link ActivityMonitor} objects only match against the first activity in * the array. @@ -1442,7 +1442,7 @@ public class Instrumentation { } /** - * Like {@link #execStartActivity(Context, IBinder, IBinder, Activity, Intent, int)}, + * Like {@link #execStartActivity}, * but accepts an array of activities to be started. Note that active * {@link ActivityMonitor} objects only match against the first activity in * the array. @@ -1545,8 +1545,7 @@ public class Instrumentation { } /** - * Like {@link #execStartActivity(Context, IBinder, IBinder, Activity, Intent, int)}, - * but for starting as a particular user. + * Like {@link #execStartActivity}, but for starting as a particular user. * * @param who The Context from which the activity is being started. * @param contextThread The main thread of the Context from which the activity @@ -1616,7 +1615,8 @@ public class Instrumentation { mUiAutomationConnection = uiAutomationConnection; } - /*package*/ static void checkStartActivityResult(int res, Object intent) { + /** @hide */ + public static void checkStartActivityResult(int res, Object intent) { if (res >= ActivityManager.START_SUCCESS) { return; } @@ -1640,6 +1640,9 @@ public class Instrumentation { case ActivityManager.START_NOT_ACTIVITY: throw new IllegalArgumentException( "PendingIntent is not an activity"); + case ActivityManager.START_NOT_VOICE_COMPATIBLE: + throw new SecurityException( + "Starting under voice control not allowed for: " + intent); default: throw new AndroidRuntimeException("Unknown error code " + res + " when starting " + intent); 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 55e7470..25a1493 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 a8896c2..4427ce1 100644 --- a/core/java/android/app/SharedPreferencesImpl.java +++ b/core/java/android/app/SharedPreferencesImpl.java @@ -45,7 +45,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.IoUtils; diff --git a/core/java/android/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java index 2045ed8..ce5306f 100644 --- a/core/java/android/app/StatusBarManager.java +++ b/core/java/android/app/StatusBarManager.java @@ -38,6 +38,7 @@ 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_SYSTEM_INFO = View.STATUS_BAR_DISABLE_SYSTEM_INFO; @@ -59,6 +60,7 @@ public class StatusBarManager { | DISABLE_SEARCH; public static final int NAVIGATION_HINT_BACK_ALT = 1 << 0; + public static final int NAVIGATION_HINT_IME_SHOWN = 1 << 1; public static final int WINDOW_STATUS_BAR = 1; public static final int WINDOW_NAVIGATION_BAR = 2; 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/VoiceInteractor.java b/core/java/android/app/VoiceInteractor.java new file mode 100644 index 0000000..6dc48b0 --- /dev/null +++ b/core/java/android/app/VoiceInteractor.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 android.app; + +import android.content.Context; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.util.ArrayMap; +import android.util.Log; +import com.android.internal.app.IVoiceInteractor; +import com.android.internal.app.IVoiceInteractorCallback; +import com.android.internal.app.IVoiceInteractorRequest; +import com.android.internal.os.HandlerCaller; +import com.android.internal.os.SomeArgs; + +import java.util.WeakHashMap; + +/** + * Interface for an {@link Activity} to interact with the user through voice. + */ +public class VoiceInteractor { + static final String TAG = "VoiceInteractor"; + static final boolean DEBUG = true; + + final Context mContext; + final Activity mActivity; + final IVoiceInteractor mInteractor; + final HandlerCaller mHandlerCaller; + final HandlerCaller.Callback mHandlerCallerCallback = new HandlerCaller.Callback() { + @Override + public void executeMessage(Message msg) { + SomeArgs args = (SomeArgs)msg.obj; + Request request; + switch (msg.what) { + case MSG_CONFIRMATION_RESULT: + request = pullRequest((IVoiceInteractorRequest)args.arg1, true); + if (DEBUG) Log.d(TAG, "onConfirmResult: req=" + + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request + + " confirmed=" + msg.arg1 + " result=" + args.arg2); + if (request != null) { + ((ConfirmationRequest)request).onConfirmationResult(msg.arg1 != 0, + (Bundle) args.arg2); + request.clear(); + } + break; + case MSG_COMMAND_RESULT: + request = pullRequest((IVoiceInteractorRequest)args.arg1, msg.arg1 != 0); + if (DEBUG) Log.d(TAG, "onCommandResult: req=" + + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request + + " result=" + args.arg2); + if (request != null) { + ((CommandRequest)request).onCommandResult((Bundle) args.arg2); + if (msg.arg1 != 0) { + request.clear(); + } + } + break; + case MSG_CANCEL_RESULT: + request = pullRequest((IVoiceInteractorRequest)args.arg1, true); + if (DEBUG) Log.d(TAG, "onCancelResult: req=" + + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request); + if (request != null) { + request.onCancel(); + request.clear(); + } + break; + } + } + }; + + final IVoiceInteractorCallback.Stub mCallback = new IVoiceInteractorCallback.Stub() { + @Override + public void deliverConfirmationResult(IVoiceInteractorRequest request, boolean confirmed, + Bundle result) { + mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIOO( + MSG_CONFIRMATION_RESULT, confirmed ? 1 : 0, request, result)); + } + + @Override + public void deliverCommandResult(IVoiceInteractorRequest request, boolean complete, + Bundle result) { + mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIOO( + MSG_COMMAND_RESULT, complete ? 1 : 0, request, result)); + } + + @Override + public void deliverCancel(IVoiceInteractorRequest request) throws RemoteException { + mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageO( + MSG_CANCEL_RESULT, request)); + } + }; + + final ArrayMap<IBinder, Request> mActiveRequests = new ArrayMap<IBinder, Request>(); + + static final int MSG_CONFIRMATION_RESULT = 1; + static final int MSG_COMMAND_RESULT = 2; + static final int MSG_CANCEL_RESULT = 3; + + public static abstract class Request { + IVoiceInteractorRequest mRequestInterface; + Context mContext; + Activity mActivity; + + public Request() { + } + + public void cancel() { + try { + mRequestInterface.cancel(); + } catch (RemoteException e) { + Log.w(TAG, "Voice interactor has died", e); + } + } + + public Context getContext() { + return mContext; + } + + public Activity getActivity() { + return mActivity; + } + + public void onCancel() { + } + + void clear() { + mRequestInterface = null; + mContext = null; + mActivity = null; + } + + abstract IVoiceInteractorRequest submit(IVoiceInteractor interactor, + String packageName, IVoiceInteractorCallback callback) throws RemoteException; + } + + public static class ConfirmationRequest extends Request { + final CharSequence mPrompt; + final Bundle mExtras; + + /** + * Confirms an operation with the user via the trusted system + * VoiceInteractionService. This allows an Activity to complete an unsafe operation that + * would require the user to touch the screen when voice interaction mode is not enabled. + * The result of the confirmation will be returned through an asynchronous call to + * either {@link #onConfirmationResult(boolean, android.os.Bundle)} or + * {@link #onCancel()}. + * + * <p>In some cases this may be a simple yes / no confirmation or the confirmation could + * include context information about how the action will be completed + * (e.g. booking a cab might include details about how long until the cab arrives) + * so the user can give a confirmation. + * @param prompt Optional confirmation text to read to the user as the action being + * confirmed. + * @param extras Additional optional information. + */ + public ConfirmationRequest(CharSequence prompt, Bundle extras) { + mPrompt = prompt; + mExtras = extras; + } + + public void onConfirmationResult(boolean confirmed, Bundle result) { + } + + IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName, + IVoiceInteractorCallback callback) throws RemoteException { + return interactor.startConfirmation(packageName, callback, mPrompt.toString(), mExtras); + } + } + + public static class CommandRequest extends Request { + final String mCommand; + final Bundle mArgs; + + /** + * Execute a command using the trusted system VoiceInteractionService. + * This allows an Activity to request additional information from the user needed to + * complete an action (e.g. booking a table might have several possible times that the + * user could select from or an app might need the user to agree to a terms of service). + * The result of the confirmation will be returned through an asynchronous call to + * either {@link #onCommandResult(android.os.Bundle)} or + * {@link #onCancel()}. + * + * <p>The command is a string that describes the generic operation to be performed. + * The command will determine how the properties in extras are interpreted and the set of + * available commands is expected to grow over time. An example might be + * "com.google.voice.commands.REQUEST_NUMBER_BAGS" to request the number of bags as part of + * airline check-in. (This is not an actual working example.) + * + * @param command The desired command to perform. + * @param args Additional arguments to control execution of the command. + */ + public CommandRequest(String command, Bundle args) { + mCommand = command; + mArgs = args; + } + + public void onCommandResult(Bundle result) { + } + + IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName, + IVoiceInteractorCallback callback) throws RemoteException { + return interactor.startConfirmation(packageName, callback, mCommand, mArgs); + } + } + + VoiceInteractor(Context context, Activity activity, IVoiceInteractor interactor, + Looper looper) { + mContext = context; + mActivity = activity; + mInteractor = interactor; + mHandlerCaller = new HandlerCaller(context, looper, mHandlerCallerCallback, true); + } + + Request pullRequest(IVoiceInteractorRequest request, boolean complete) { + synchronized (mActiveRequests) { + Request req = mActiveRequests.get(request.asBinder()); + if (req != null && complete) { + mActiveRequests.remove(request.asBinder()); + } + return req; + } + } + + public boolean submitRequest(Request request) { + try { + IVoiceInteractorRequest ireq = request.submit(mInteractor, + mContext.getOpPackageName(), mCallback); + request.mRequestInterface = ireq; + request.mContext = mContext; + request.mActivity = mActivity; + synchronized (mActiveRequests) { + mActiveRequests.put(ireq.asBinder(), request); + } + return true; + } catch (RemoteException e) { + Log.w(TAG, "Remove voice interactor service died", e); + return false; + } + } + + /** + * Queries the supported commands available from the VoiceinteractionService. + * The command is a string that describes the generic operation to be performed. + * An example might be "com.google.voice.commands.REQUEST_NUMBER_BAGS" to request the number + * of bags as part of airline check-in. (This is not an actual working example.) + * + * @param commands + */ + public boolean[] supportsCommands(String[] commands) { + try { + boolean[] res = mInteractor.supportsCommands(mContext.getOpPackageName(), commands); + if (DEBUG) Log.d(TAG, "supportsCommands: cmds=" + commands + " res=" + res); + return res; + } catch (RemoteException e) { + throw new RuntimeException("Voice interactor has died", e); + } + } +} diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java index 3a766b7..58d707c 100644 --- a/core/java/android/app/WallpaperManager.java +++ b/core/java/android/app/WallpaperManager.java @@ -16,6 +16,7 @@ package android.app; +import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; @@ -41,14 +42,17 @@ 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; +import android.os.SystemProperties; +import android.text.TextUtils; import android.util.Log; import android.view.WindowManagerGlobal; import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; @@ -66,6 +70,11 @@ public class WallpaperManager { private float mWallpaperXStep = -1; private float mWallpaperYStep = -1; + /** {@hide} */ + private static final String PROP_WALLPAPER = "ro.config.wallpaper"; + /** {@hide} */ + private static final String PROP_WALLPAPER_COMPONENT = "ro.config.wallpaper_component"; + /** * Activity Action: Show settings for choosing wallpaper. Do not use directly to construct * an intent; instead, use {@link #getCropAndSetWallpaperIntent}. @@ -219,24 +228,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 +239,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 +275,6 @@ public class WallpaperManager { synchronized (this) { mWallpaper = null; mDefaultWallpaper = null; - mHandler.removeMessages(MSG_CLEAR_WALLPAPER); } } @@ -313,8 +309,7 @@ public class WallpaperManager { } private Bitmap getDefaultWallpaperLocked(Context context) { - InputStream is = context.getResources().openRawResource( - com.android.internal.R.drawable.default_wallpaper); + InputStream is = openDefaultWallpaper(context); if (is != null) { try { BitmapFactory.Options options = new BitmapFactory.Options(); @@ -412,8 +407,7 @@ public class WallpaperManager { horizontalAlignment = Math.max(0, Math.min(1, horizontalAlignment)); verticalAlignment = Math.max(0, Math.min(1, verticalAlignment)); - InputStream is = new BufferedInputStream( - resources.openRawResource(com.android.internal.R.drawable.default_wallpaper)); + InputStream is = new BufferedInputStream(openDefaultWallpaper(mContext)); if (is == null) { Log.e(TAG, "default wallpaper input stream is null"); @@ -438,8 +432,7 @@ public class WallpaperManager { } } - is = new BufferedInputStream(resources.openRawResource( - com.android.internal.R.drawable.default_wallpaper)); + is = new BufferedInputStream(openDefaultWallpaper(mContext)); RectF cropRectF; @@ -488,8 +481,7 @@ public class WallpaperManager { if (crop == null) { // BitmapRegionDecoder has failed, try to crop in-memory - is = new BufferedInputStream(resources.openRawResource( - com.android.internal.R.drawable.default_wallpaper)); + is = new BufferedInputStream(openDefaultWallpaper(mContext)); Bitmap fullSize = null; if (is != null) { BitmapFactory.Options options = new BitmapFactory.Options(); @@ -1022,6 +1014,53 @@ public class WallpaperManager { * wallpaper. */ public void clear() throws IOException { - setResource(com.android.internal.R.drawable.default_wallpaper); + setStream(openDefaultWallpaper(mContext)); + } + + /** + * Open stream representing the default static image wallpaper. + * + * @hide + */ + public static InputStream openDefaultWallpaper(Context context) { + final String path = SystemProperties.get(PROP_WALLPAPER); + if (!TextUtils.isEmpty(path)) { + final File file = new File(path); + if (file.exists()) { + try { + return new FileInputStream(file); + } catch (IOException e) { + // Ignored, fall back to platform default below + } + } + } + return context.getResources().openRawResource( + com.android.internal.R.drawable.default_wallpaper); + } + + /** + * Return {@link ComponentName} of the default live wallpaper, or + * {@code null} if none is defined. + * + * @hide + */ + public static ComponentName getDefaultWallpaperComponent(Context context) { + String flat = SystemProperties.get(PROP_WALLPAPER_COMPONENT); + if (!TextUtils.isEmpty(flat)) { + final ComponentName cn = ComponentName.unflattenFromString(flat); + if (cn != null) { + return cn; + } + } + + flat = context.getString(com.android.internal.R.string.default_wallpaper_component); + if (!TextUtils.isEmpty(flat)) { + final ComponentName cn = ComponentName.unflattenFromString(flat); + if (cn != null) { + return cn; + } + } + + return null; } } 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..6f68dfb 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -22,14 +22,18 @@ 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.Bundle; import android.os.Handler; +import android.os.Process; import android.os.RemoteCallback; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; +import android.service.trust.TrustAgentService; import android.util.Log; import com.android.org.conscrypt.TrustedCertificateStore; @@ -75,6 +79,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 @@ -131,6 +172,16 @@ public class DevicePolicyManager { @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_SET_NEW_PASSWORD = "android.app.action.SET_NEW_PASSWORD"; + /** + * Flag for {@link #forwardMatchingIntents}: the intents will forwarded to the primary user. + */ + public static int FLAG_TO_PRIMARY_USER = 0x0001; + + /** + * Flag for {@link #forwardMatchingIntents}: the intents will be forwarded to the managed + * profile. + */ + public static int FLAG_TO_MANAGED_PROFILE = 0x0002; /** * Return true if the given administrator component is currently @@ -1153,7 +1204,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) { @@ -1225,7 +1278,7 @@ public class DevicePolicyManager { public static final int KEYGUARD_DISABLE_FEATURES_NONE = 0; /** - * Disable all keyguard widgets + * Disable all keyguard widgets. Has no effect. */ public static final int KEYGUARD_DISABLE_WIDGETS_ALL = 1 << 0; @@ -1235,6 +1288,22 @@ public class DevicePolicyManager { public static final int KEYGUARD_DISABLE_SECURE_CAMERA = 1 << 1; /** + * Disable showing all notifications on secure keyguard screens (e.g. PIN/Pattern/Password) + */ + public static final int KEYGUARD_DISABLE_SECURE_NOTIFICATIONS = 1 << 2; + + /** + * Only allow redacted notifications on secure keyguard screens (e.g. PIN/Pattern/Password) + */ + public static final int KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS = 1 << 3; + + /** + * Ignore {@link TrustAgentService} state on secure keyguard screens + * (e.g. PIN/Pattern/Password). + */ + public static final int KEYGUARD_DISABLE_TRUST_AGENTS = 1 << 4; + + /** * Disable all current and future keyguard customizations. */ public static final int KEYGUARD_DISABLE_FEATURES_ALL = 0x7fffffff; @@ -1454,7 +1523,8 @@ public class DevicePolicyManager { * @param admin Which {@link DeviceAdminReceiver} this request is associated with. * @param which {@link #KEYGUARD_DISABLE_FEATURES_NONE} (default), * {@link #KEYGUARD_DISABLE_WIDGETS_ALL}, {@link #KEYGUARD_DISABLE_SECURE_CAMERA}, - * {@link #KEYGUARD_DISABLE_FEATURES_ALL} + * {@link #KEYGUARD_DISABLE_SECURE_NOTIFICATIONS}, {@link #KEYGUARD_DISABLE_TRUST_AGENTS}, + * {@link #KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS}, {@link #KEYGUARD_DISABLE_FEATURES_ALL} */ public void setKeyguardDisabledFeatures(ComponentName admin, int which) { if (mService != null) { @@ -1493,10 +1563,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 +1574,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 +1758,287 @@ 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); + } + } + } + + /** + * Called by a profile or device owner to set the application restrictions for a given target + * application running in the managed profile. + * + * <p>The provided {@link Bundle} consists of key-value pairs, where the types of values may be + * {@link Boolean}, {@link String}, or {@link String}[]. The recommended format for key strings + * is "com.example.packagename/example-setting" to avoid naming conflicts with library + * components such as {@link android.webkit.WebView}. + * + * <p>The application restrictions are only made visible to the target application and the + * profile or device owner. + * + * <p>The calling device admin must be a profile or device 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 to update restricted settings for. + * @param settings A {@link Bundle} to be parsed by the receiving application, conveying a new + * set of active restrictions. + */ + public void setApplicationRestrictions(ComponentName admin, String packageName, + Bundle settings) { + if (mService != null) { + try { + mService.setApplicationRestrictions(admin, packageName, settings); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + } + + /** + * Called by a profile owner to forward intents sent from the managed profile to the owner, or + * from the owner to the managed profile. + * If an intent matches this intent filter, then activities belonging to the other user can + * respond to this intent. + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * @param filter if an intent matches this IntentFilter, then it can be forwarded. + */ + public void forwardMatchingIntents(ComponentName admin, IntentFilter filter, int flags) { + if (mService != null) { + try { + mService.forwardMatchingIntents(admin, filter, flags); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + } + + /** + * Called by a profile owner to remove all the forwarding intent filters from the current user + * and from the owner. + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + */ + public void clearForwardingIntentFilters(ComponentName admin) { + if (mService != null) { + try { + mService.clearForwardingIntentFilters(admin); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + } + + /** + * Called by a profile or device owner to get the application restrictions for a given target + * application running in the managed profile. + * + * <p>The calling device admin must be a profile or device 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 to fetch restricted settings of. + * @return {@link Bundle} of settings corresponding to what was set last time + * {@link DevicePolicyManager#setApplicationRestrictions} was called, or an empty {@link Bundle} + * if no restrictions have been set. + */ + public Bundle getApplicationRestrictions(ComponentName admin, String packageName) { + if (mService != null) { + try { + return mService.getApplicationRestrictions(admin, packageName); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + return null; + } + + /** + * Called by a profile or device owner to set a user restriction specified + * by the key. + * <p> + * The calling device admin must be a profile or device owner; if it is not, + * a security exception will be thrown. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated + * with. + * @param key The key of the restriction. See the constants in + * {@link android.os.UserManager} for the list of keys. + */ + public void addUserRestriction(ComponentName admin, String key) { + if (mService != null) { + try { + mService.setUserRestriction(admin, key, true); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + } + + /** + * Called by a profile or device owner to clear a user restriction specified + * by the key. + * <p> + * The calling device admin must be a profile or device owner; if it is not, + * a security exception will be thrown. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated + * with. + * @param key The key of the restriction. See the constants in + * {@link android.os.UserManager} for the list of keys. + */ + public void clearUserRestriction(ComponentName admin, String key) { + if (mService != null) { + try { + mService.setUserRestriction(admin, key, false); + } 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..495a5f9 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -18,6 +18,8 @@ package android.app.admin; import android.content.ComponentName; +import android.content.IntentFilter; +import android.os.Bundle; import android.os.RemoteCallback; /** @@ -103,6 +105,21 @@ 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); + + void setApplicationRestrictions(in ComponentName who, in String packageName, in Bundle settings); + Bundle getApplicationRestrictions(in ComponentName who, in String packageName); + + void setUserRestriction(in ComponentName who, in String key, boolean enable); + void forwardMatchingIntents(in ComponentName admin, in IntentFilter filter, int flags); + void clearForwardingIntentFilters(in ComponentName admin); } diff --git a/core/java/android/app/backup/BackupAgent.java b/core/java/android/app/backup/BackupAgent.java index 70a3797..886f1a6 100644 --- a/core/java/android/app/backup/BackupAgent.java +++ b/core/java/android/app/backup/BackupAgent.java @@ -127,6 +127,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); @@ -139,12 +146,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 */ } @@ -679,5 +683,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 477285d..6ebb6c4 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.system.ErrnoException; import android.system.Os; 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/app/task/ITaskCallback.aidl b/core/java/android/app/task/ITaskCallback.aidl new file mode 100644 index 0000000..ffa57d1 --- /dev/null +++ b/core/java/android/app/task/ITaskCallback.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.task; + +import android.app.task.ITaskService; +import android.app.task.TaskParams; + +/** + * The server side of the TaskManager 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 Task Service instance is reporting. + * + * {@hide} + */ +interface ITaskCallback { + /** + * Immediate callback to the system after sending a start signal, used to quickly detect ANR. + * + * @param taskId Unique integer used to identify this task. + */ + void acknowledgeStartMessage(int taskId); + /** + * Immediate callback to the system after sending a stop signal, used to quickly detect ANR. + * + * @param taskId Unique integer used to identify this task. + */ + void acknowledgeStopMessage(int taskId); + /* + * Tell the task manager that the client is done with its execution, so that it can go on to + * the next one and stop attributing wakelock time to us etc. + * + * @param taskId Unique integer used to identify this task. + * @param reschedule Whether or not to reschedule this task. + */ + void taskFinished(int taskId, boolean reschedule); +} diff --git a/core/java/android/app/task/ITaskService.aidl b/core/java/android/app/task/ITaskService.aidl new file mode 100644 index 0000000..87b0191 --- /dev/null +++ b/core/java/android/app/task/ITaskService.aidl @@ -0,0 +1,35 @@ +/** + * 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.task; + +import android.app.task.ITaskCallback; +import android.app.task.TaskParams; + +import android.os.Bundle; + +/** + * Interface that the framework uses to communicate with application code that implements a + * TaskService. End user code does not implement this interface directly; instead, the app's + * service implementation will extend android.app.task.TaskService. + * {@hide} + */ +oneway interface ITaskService { + /** Begin execution of application's task. */ + void startTask(in TaskParams taskParams); + /** Stop execution of application's task. */ + void stopTask(in TaskParams taskParams); +} diff --git a/core/java/android/app/task/TaskParams.aidl b/core/java/android/app/task/TaskParams.aidl new file mode 100644 index 0000000..9b25855 --- /dev/null +++ b/core/java/android/app/task/TaskParams.aidl @@ -0,0 +1,19 @@ +/** + * 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.task; + +parcelable TaskParams;
\ No newline at end of file diff --git a/core/java/android/app/task/TaskParams.java b/core/java/android/app/task/TaskParams.java new file mode 100644 index 0000000..e2eafd8 --- /dev/null +++ b/core/java/android/app/task/TaskParams.java @@ -0,0 +1,86 @@ +/* + * 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.task; + +import android.os.Bundle; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Contains the parameters used to configure/identify your task. You do not create this object + * yourself, instead it is handed in to your application by the System. + */ +public class TaskParams implements Parcelable { + + private final int taskId; + private final Bundle extras; + private final IBinder mCallback; + + /** + * @return The unique id of this task, specified at creation time. + */ + public int getTaskId() { + return taskId; + } + + /** + * @return The extras you passed in when constructing this task with + * {@link android.content.Task.Builder#setExtras(android.os.Bundle)}. This will + * never be null. If you did not set any extras this will be an empty bundle. + */ + public Bundle getExtras() { + return extras; + } + + /** + * @hide + */ + public ITaskCallback getCallback() { + return ITaskCallback.Stub.asInterface(mCallback); + } + + private TaskParams(Parcel in) { + taskId = in.readInt(); + extras = in.readBundle(); + mCallback = in.readStrongBinder(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(taskId); + dest.writeBundle(extras); + dest.writeStrongBinder(mCallback); + } + + public static final Creator<TaskParams> CREATOR = new Creator<TaskParams>() { + @Override + public TaskParams createFromParcel(Parcel in) { + return new TaskParams(in); + } + + @Override + public TaskParams[] newArray(int size) { + return new TaskParams[size]; + } + }; +} diff --git a/core/java/android/app/task/TaskService.java b/core/java/android/app/task/TaskService.java new file mode 100644 index 0000000..81333be --- /dev/null +++ b/core/java/android/app/task/TaskService.java @@ -0,0 +1,260 @@ +/* + * 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.task; + +import android.app.Service; +import android.content.Intent; +import android.os.Bundle; +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 com.android.internal.annotations.GuardedBy; + +/** + * <p>Entry point for the callback from the {@link android.content.TaskManager}.</p> + * <p>This is the base class that handles asynchronous requests that were previously scheduled. You + * are responsible for overriding {@link TaskService#onStartTask(TaskParams)}, which is where + * you will implement your task logic.</p> + * <p>This service executes each incoming task on a {@link android.os.Handler} running on your + * application's main thread. This means that you <b>must</b> offload your execution logic to + * another thread/handler/{@link android.os.AsyncTask} of your choosing. Not doing so will result + * in blocking any future callbacks from the TaskManager - specifically + * {@link #onStopTask(android.app.task.TaskParams)}, which is meant to inform you that the + * scheduling requirements are no longer being met.</p> + */ +public abstract class TaskService extends Service { + private static final String TAG = "TaskService"; + + /** + * Task services must be protected with this permission: + * + * <pre class="prettyprint"> + * <service android:name="MyTaskService" + * android:permission="android.permission.BIND_TASK_SERVICE" > + * ... + * </service> + * </pre> + * + * <p>If a task 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_TASK_SERVICE"; + + /** + * Identifier for a message that will result in a call to + * {@link #onStartTask(android.app.task.TaskParams)}. + */ + private final int MSG_EXECUTE_TASK = 0; + /** + * Message that will result in a call to {@link #onStopTask(android.app.task.TaskParams)}. + */ + private final int MSG_STOP_TASK = 1; + /** + * Message that the client has completed execution of this task. + */ + private final int MSG_TASK_FINISHED = 2; + + /** Lock object for {@link #mHandler}. */ + private final Object mHandlerLock = new Object(); + + /** + * Handler we post tasks to. Responsible for calling into the client logic, and handling the + * callback to the system. + */ + @GuardedBy("mHandlerLock") + TaskHandler mHandler; + + /** Binder for this service. */ + ITaskService mBinder = new ITaskService.Stub() { + @Override + public void startTask(TaskParams taskParams) { + ensureHandler(); + Message m = Message.obtain(mHandler, MSG_EXECUTE_TASK, taskParams); + m.sendToTarget(); + } + @Override + public void stopTask(TaskParams taskParams) { + ensureHandler(); + Message m = Message.obtain(mHandler, MSG_STOP_TASK, taskParams); + m.sendToTarget(); + } + }; + + /** @hide */ + void ensureHandler() { + synchronized (mHandlerLock) { + if (mHandler == null) { + mHandler = new TaskHandler(getMainLooper()); + } + } + } + + /** + * Runs on application's main thread - callbacks are meant to offboard work to some other + * (app-specified) mechanism. + * @hide + */ + class TaskHandler extends Handler { + TaskHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + final TaskParams params = (TaskParams) msg.obj; + switch (msg.what) { + case MSG_EXECUTE_TASK: + try { + TaskService.this.onStartTask(params); + } catch (Exception e) { + Log.e(TAG, "Error while executing task: " + params.getTaskId()); + throw new RuntimeException(e); + } finally { + maybeAckMessageReceived(params, MSG_EXECUTE_TASK); + } + break; + case MSG_STOP_TASK: + try { + TaskService.this.onStopTask(params); + } catch (Exception e) { + Log.e(TAG, "Application unable to handle onStopTask.", e); + throw new RuntimeException(e); + } finally { + maybeAckMessageReceived(params, MSG_STOP_TASK); + } + break; + case MSG_TASK_FINISHED: + final boolean needsReschedule = (msg.arg2 == 1); + ITaskCallback callback = params.getCallback(); + if (callback != null) { + try { + callback.taskFinished(params.getTaskId(), needsReschedule); + } catch (RemoteException e) { + Log.e(TAG, "Error reporting task finish to system: binder has gone" + + "away."); + } + } else { + Log.e(TAG, "finishTask() called for a nonexistent task id."); + } + break; + default: + Log.e(TAG, "Unrecognised message received."); + break; + } + } + + /** + * Messages come in on the application's main thread, so rather than run the risk of + * waiting for an app that may be doing something foolhardy, we ack to the system after + * processing a message. This allows us to throw up an ANR dialogue as quickly as possible. + * @param params id of the task we're acking. + * @param state Information about what message we're acking. + */ + private void maybeAckMessageReceived(TaskParams params, int state) { + final ITaskCallback callback = params.getCallback(); + final int taskId = params.getTaskId(); + if (callback != null) { + try { + if (state == MSG_EXECUTE_TASK) { + callback.acknowledgeStartMessage(taskId); + } else if (state == MSG_STOP_TASK) { + callback.acknowledgeStopMessage(taskId); + } + } catch(RemoteException e) { + Log.e(TAG, "System unreachable for starting task."); + } + } else { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, state + ": Attempting to ack a task that has already been" + + "processed."); + } + } + } + } + + /** @hide */ + public final IBinder onBind(Intent intent) { + return mBinder.asBinder(); + } + + /** + * Override this method with the callback logic for your task. Any such logic needs to be + * performed on a separate thread, as this function is executed on your application's main + * thread. + * + * @param params Parameters specifying info about this task, including the extras bundle you + * optionally provided at task-creation time. + */ + public abstract void onStartTask(TaskParams params); + + /** + * This method is called if your task should be stopped even before you've called + * {@link #taskFinished(TaskParams, boolean)}. + * + * <p>This will happen if the requirements specified at schedule time are no longer met. For + * example you may have requested WiFi with + * {@link android.content.Task.Builder#setRequiredNetworkCapabilities(int)}, yet while your + * task was executing the user toggled WiFi. Another example is if you had specified + * {@link android.content.Task.Builder#setRequiresDeviceIdle(boolean)}, and the phone left its + * idle maintenance window. You are solely responsible for the behaviour of your application + * upon receipt of this message; your app will likely start to misbehave if you ignore it. One + * repercussion is that the system will cease to hold a wakelock for you.</p> + * + * <p>After you've done your clean-up you are still expected to call + * {@link #taskFinished(TaskParams, boolean)} this will inform the TaskManager that all is well, and + * allow you to reschedule your task as it is probably uncompleted. Until you call + * taskFinished() you will not receive any newly scheduled tasks with the given task id as the + * TaskManager will consider the task to be in an error state.</p> + * + * @param params Parameters specifying info about this task. + * @return True to indicate to the TaskManager whether you'd like to reschedule this task based + * on the criteria provided at task creation-time. False to drop the task. Regardless of the + * value returned, your task must stop executing. + */ + public abstract boolean onStopTask(TaskParams params); + + /** + * Callback to inform the TaskManager you have completed execution. This can be called from any + * thread, as it will ultimately be run on your application's main thread. When the system + * receives this message it will release the wakelock being held. + * <p> + * You can specify post-execution behaviour to the scheduler here with <code>needsReschedule + * </code>. This will apply a back-off timer to your task based on the default, or what was + * set with {@link android.content.Task.Builder#setBackoffCriteria(long, int)}. The + * original requirements are always honoured even for a backed-off task. + * Note that a task running in idle mode will not be backed-off. Instead what will happen + * is the task will be re-added to the queue and re-executed within a future idle + * maintenance window. + * </p> + * + * @param params Parameters specifying system-provided info about this task, this was given to + * your application in {@link #onStartTask(TaskParams)}. + * @param needsReschedule True if this task is complete, false if you want the TaskManager to + * reschedule you. + */ + public final void taskFinished(TaskParams params, boolean needsReschedule) { + ensureHandler(); + Message m = Message.obtain(mHandler, MSG_TASK_FINISHED, params); + m.arg2 = needsReschedule ? 1 : 0; + m.sendToTarget(); + } +}
\ No newline at end of file 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 7745bb7..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; @@ -61,9 +53,6 @@ public class BluetoothTetheringDataTracker extends BaseNetworkStateTracker { private static final boolean DBG = true; private static final boolean VDBG = true; - // Event sent to the mBtdtHandler when DHCP fails so we can tear down the network. - private static final int EVENT_NETWORK_FAILED = 1; - private AtomicBoolean mTeardownRequested = new AtomicBoolean(false); private AtomicBoolean mPrivateDnsRouteSet = new AtomicBoolean(false); private AtomicInteger mDefaultGatewayAddr = new AtomicInteger(0); @@ -146,11 +135,6 @@ public class BluetoothTetheringDataTracker extends BaseNetworkStateTracker { } @Override - public void captivePortalCheckComplete() { - // not implemented - } - - @Override public void captivePortalCheckCompleted(boolean isCaptivePortal) { // not implemented } @@ -331,7 +315,6 @@ public class BluetoothTetheringDataTracker extends BaseNetworkStateTracker { } if (!success) { Log.e(TAG, "DHCP request error:" + NetworkUtils.getDhcpError()); - mBtdtHandler.obtainMessage(EVENT_NETWORK_FAILED).sendToTarget(); return; } mLinkProperties = dhcpResults.linkProperties; @@ -424,10 +407,6 @@ public class BluetoothTetheringDataTracker extends BaseNetworkStateTracker { if (VDBG) Log.d(TAG, "got EVENT_NETWORK_DISCONNECTED, " + linkProperties); mBtdt.stopReverseTether(); break; - case EVENT_NETWORK_FAILED: - if (VDBG) Log.d(TAG, "got EVENT_NETWORK_FAILED"); - mBtdt.teardown(); - break; } } } 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..5b41394 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> @@ -1636,7 +1641,7 @@ public abstract class ContentResolver { * * @see #getPersistedUriPermissions() */ - public void takePersistableUriPermission(Uri uri, int modeFlags) { + public void takePersistableUriPermission(Uri uri, @Intent.AccessUriMode int modeFlags) { try { ActivityManagerNative.getDefault().takePersistableUriPermission(uri, modeFlags); } catch (RemoteException e) { @@ -1651,7 +1656,7 @@ public abstract class ContentResolver { * * @see #getPersistedUriPermissions() */ - public void releasePersistableUriPermission(Uri uri, int modeFlags) { + public void releasePersistableUriPermission(Uri uri, @Intent.AccessUriMode int modeFlags) { try { ActivityManagerNative.getDefault().releasePersistableUriPermission(uri, modeFlags); } catch (RemoteException e) { @@ -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..de223a3 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.SessionManager} for managing media Sessions. + * + * @see #getSystemService + * @see android.media.session.SessionManager + */ + 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. * @@ -2307,6 +2442,14 @@ public abstract class Context { public static final String APPWIDGET_SERVICE = "appwidget"; /** + * Official published name of the (internal) voice interaction manager service. + * + * @hide + * @see #getSystemService + */ + public static final String VOICE_INTERACTION_MANAGER_SERVICE = "voiceinteraction"; + + /** * Use with {@link #getSystemService} to retrieve an * {@link android.app.backup.IBackupManager IBackupManager} for communicating * with the backup mechanism. @@ -2351,6 +2494,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 +2585,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 +2610,6 @@ public abstract class Context { * * @see #getSystemService * @see android.hardware.camera2.CameraManager - * @hide */ public static final String CAMERA_SERVICE = "camera"; @@ -2473,6 +2633,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 +2674,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 +2698,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 +2717,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 +2733,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 +2754,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 +2770,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 @@ -2610,14 +2799,18 @@ public abstract class Context { * @param uri The Uri you would like to grant access to. * @param modeFlags The desired access modes. Any combination of * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION - * Intent.FLAG_GRANT_READ_URI_PERMISSION} or + * Intent.FLAG_GRANT_READ_URI_PERMISSION}, * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION - * Intent.FLAG_GRANT_WRITE_URI_PERMISSION}. + * Intent.FLAG_GRANT_WRITE_URI_PERMISSION}, + * {@link Intent#FLAG_GRANT_PERSISTABLE_URI_PERMISSION + * Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION}, or + * {@link Intent#FLAG_GRANT_PREFIX_URI_PERMISSION + * Intent.FLAG_GRANT_PREFIX_URI_PERMISSION}. * * @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 @@ -2625,7 +2818,8 @@ public abstract class Context { * Uri will match all previously granted Uris that are the same or a * sub-path of the given Uri. That is, revoking "content://foo/target" will * revoke both "content://foo/target" and "content://foo/target/sub", but not - * "content://foo". + * "content://foo". It will not remove any prefix grants that exist at a + * higher level. * * @param uri The Uri you would like to revoke access to. * @param modeFlags The desired access modes. Any combination of @@ -2636,7 +2830,7 @@ public abstract class Context { * * @see #grantUriPermission */ - public abstract void revokeUriPermission(Uri uri, int modeFlags); + public abstract void revokeUriPermission(Uri uri, @Intent.AccessUriMode int modeFlags); /** * Determine whether a particular process and user ID has been granted @@ -2659,7 +2853,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.AccessUriMode int modeFlags); /** * Determine whether the calling process and user ID has been @@ -2682,7 +2877,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.AccessUriMode int modeFlags); /** * Determine whether the calling process of an IPC <em>or you</em> has been granted @@ -2701,7 +2896,8 @@ public abstract class Context { * * @see #checkCallingUriPermission */ - public abstract int checkCallingOrSelfUriPermission(Uri uri, int modeFlags); + public abstract int checkCallingOrSelfUriPermission(Uri uri, + @Intent.AccessUriMode int modeFlags); /** * Check both a Uri and normal permission. This allows you to perform @@ -2713,7 +2909,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 +2921,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.AccessUriMode int modeFlags); /** * If a particular process and user ID has not been granted @@ -2748,7 +2945,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.AccessUriMode int modeFlags, String message); /** * If the calling process and user ID has not been granted @@ -2770,7 +2967,7 @@ public abstract class Context { * @see #checkCallingUriPermission(Uri, int) */ public abstract void enforceCallingUriPermission( - Uri uri, int modeFlags, String message); + Uri uri, @Intent.AccessUriMode int modeFlags, String message); /** * If the calling process of an IPC <em>or you</em> has not been @@ -2789,7 +2986,7 @@ public abstract class Context { * @see #checkCallingOrSelfUriPermission(Uri, int) */ public abstract void enforceCallingOrSelfUriPermission( - Uri uri, int modeFlags, String message); + Uri uri, @Intent.AccessUriMode int modeFlags, String message); /** * Enforce both a Uri and normal permission. This allows you to perform @@ -2801,7 +2998,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 +3010,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.AccessUriMode 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 +3076,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 +3112,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 +3133,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..ae5437b 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -18,9 +18,11 @@ package android.content; import android.content.pm.ApplicationInfo; 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 +47,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; @@ -861,8 +865,9 @@ public class Intent implements Parcelable, Cloneable { } // Migrate any clip data and flags from target. - int permFlags = target.getFlags() - & (FLAG_GRANT_READ_URI_PERMISSION | FLAG_GRANT_WRITE_URI_PERMISSION); + int permFlags = target.getFlags() & (FLAG_GRANT_READ_URI_PERMISSION + | FLAG_GRANT_WRITE_URI_PERMISSION | FLAG_GRANT_PERSISTABLE_URI_PERMISSION + | FLAG_GRANT_PREFIX_URI_PERMISSION); if (permFlags != 0) { ClipData targetClipData = target.getClipData(); if (targetClipData == null && target.getData() != null) { @@ -2193,6 +2198,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> @@ -2298,6 +2308,16 @@ public class Intent implements Parcelable, Cloneable { = "android.intent.action.ADVANCED_SETTINGS"; /** + * Broadcast Action: Sent after application restrictions are changed. + * + * <p class="note">This is a protected intent that can only be sent + * by the system.</p> + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_APPLICATION_RESTRICTIONS_CHANGED = + "android.intent.action.APPLICATION_RESTRICTIONS_CHANGED"; + + /** * Broadcast Action: An outgoing call is about to be placed. * * <p>The Intent will have the following extra value:</p> @@ -2623,6 +2643,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 = @@ -2663,9 +2700,11 @@ public class Intent implements Parcelable, Cloneable { * take the persistable permissions using * {@link ContentResolver#takePersistableUriPermission(Uri, int)}. * <p> - * Callers can restrict document selection to a specific kind of data, such - * as photos, by setting one or more MIME types in - * {@link #EXTRA_MIME_TYPES}. + * Callers must indicate the acceptable document MIME types through + * {@link #setType(String)}. For example, to select photos, use + * {@code image/*}. If multiple disjoint MIME types are acceptable, define + * them in {@link #EXTRA_MIME_TYPES} and {@link #setType(String)} to + * {@literal *}/*. * <p> * If the caller can handle multiple returned items (the user performing * multiple selection), then you can specify {@link #EXTRA_ALLOW_MULTIPLE} @@ -2675,9 +2714,10 @@ public class Intent implements Parcelable, Cloneable { * returned URIs can be opened with * {@link ContentResolver#openFileDescriptor(Uri, String)}. * <p> - * Output: The URI of the item that was picked. This must be a - * {@code content://} URI so that any receiver can access it. If multiple - * documents were selected, they are returned in {@link #getClipData()}. + * Output: The URI of the item that was picked, returned in + * {@link #getData()}. This must be a {@code content://} URI so that any + * receiver can access it. If multiple documents were selected, they are + * returned in {@link #getClipData()}. * * @see DocumentsContract * @see #ACTION_CREATE_DOCUMENT @@ -2719,6 +2759,24 @@ public class Intent implements Parcelable, Cloneable { @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_CREATE_DOCUMENT = "android.intent.action.CREATE_DOCUMENT"; + /** + * Activity Action: Allow the user to pick a directory. When invoked, the + * system will display the various {@link DocumentsProvider} instances + * installed on the device, letting the user navigate through them. Apps can + * fully manage documents within the returned directory. + * <p> + * To gain access to descendant (child, grandchild, etc) documents, use + * {@link DocumentsContract#buildDocumentViaUri(Uri, String)} and + * {@link DocumentsContract#buildChildDocumentsViaUri(Uri, String)} using + * the returned directory URI. + * <p> + * Output: The URI representing the selected directory. + * + * @see DocumentsContract + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_PICK_DIRECTORY = "android.intent.action.PICK_DIRECTORY"; + // --------------------------------------------------------------------- // --------------------------------------------------------------------- // Standard intent categories (see addCategory()). @@ -2746,6 +2804,14 @@ public class Intent implements Parcelable, Cloneable { @SdkConstant(SdkConstantType.INTENT_CATEGORY) public static final String CATEGORY_BROWSABLE = "android.intent.category.BROWSABLE"; /** + * Categories for activities that can participate in voice interaction. + * An activity that supports this category must be prepared to run with + * no UI shown at all (though in some case it may have a UI shown), and + * rely on {@link android.app.VoiceInteractor} to interact with the user. + */ + @SdkConstant(SdkConstantType.INTENT_CATEGORY) + public static final String CATEGORY_VOICE = "android.intent.category.VOICE"; + /** * Set if the activity should be considered as an alternative action to * the data the user is currently viewing. See also * {@link #CATEGORY_SELECTED_ALTERNATIVE} for an alternative action that @@ -2899,6 +2965,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()). @@ -3289,6 +3363,7 @@ public class Intent implements Parcelable, Cloneable { * @see #ACTION_GET_CONTENT * @see #ACTION_OPEN_DOCUMENT * @see #ACTION_CREATE_DOCUMENT + * @see #ACTION_PICK_DIRECTORY */ public static final String EXTRA_LOCAL_ONLY = "android.intent.extra.LOCAL_ONLY"; @@ -3308,15 +3383,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 +3456,30 @@ 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, + FLAG_GRANT_PERSISTABLE_URI_PERMISSION, FLAG_GRANT_PREFIX_URI_PERMISSION }) + @Retention(RetentionPolicy.SOURCE) + public @interface GrantUriMode {} + + /** @hide */ + @IntDef(flag = true, value = { + FLAG_GRANT_READ_URI_PERMISSION, FLAG_GRANT_WRITE_URI_PERMISSION }) + @Retention(RetentionPolicy.SOURCE) + public @interface AccessUriMode {} + + /** + * Test if given mode flags specify an access mode, which must be at least + * read and/or write. + * + * @hide + */ + public static boolean isAccessUriMode(int modeFlags) { + return (modeFlags & (Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION)) != 0; + } + /** * 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 @@ -3434,6 +3541,17 @@ public class Intent implements Parcelable, Cloneable { public static final int FLAG_GRANT_PERSISTABLE_URI_PERMISSION = 0x00000040; /** + * When combined with {@link #FLAG_GRANT_READ_URI_PERMISSION} and/or + * {@link #FLAG_GRANT_WRITE_URI_PERMISSION}, the URI permission grant + * applies to any URI that is a prefix match against the original granted + * URI. (Without this flag, the URI must match exactly for access to be + * granted.) Another URI is considered a prefix match only when scheme, + * authority, and all path segments defined by the prefix are an exact + * match. + */ + public static final int FLAG_GRANT_PREFIX_URI_PERMISSION = 0x00000080; + + /** * If set, the new activity is not kept in the history stack. As soon as * the user navigates away from it, the activity is finished. This may also * be set with the {@link android.R.styleable#AndroidManifestActivity_noHistory @@ -3471,7 +3589,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 +3610,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 +3728,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. @@ -3710,9 +3871,9 @@ public class Intent implements Parcelable, Cloneable { /** * @hide Flags that can't be changed with PendingIntent. */ - public static final int IMMUTABLE_FLAGS = - FLAG_GRANT_READ_URI_PERMISSION - | FLAG_GRANT_WRITE_URI_PERMISSION; + public static final int IMMUTABLE_FLAGS = FLAG_GRANT_READ_URI_PERMISSION + | FLAG_GRANT_WRITE_URI_PERMISSION | FLAG_GRANT_PERSISTABLE_URI_PERMISSION + | FLAG_GRANT_PREFIX_URI_PERMISSION; // --------------------------------------------------------------------- // --------------------------------------------------------------------- @@ -6250,6 +6411,8 @@ public class Intent implements Parcelable, Cloneable { * * @see #FLAG_GRANT_READ_URI_PERMISSION * @see #FLAG_GRANT_WRITE_URI_PERMISSION + * @see #FLAG_GRANT_PERSISTABLE_URI_PERMISSION + * @see #FLAG_GRANT_PREFIX_URI_PERMISSION * @see #FLAG_DEBUG_LOG_RESOLUTION * @see #FLAG_FROM_BACKGROUND * @see #FLAG_ACTIVITY_BROUGHT_TO_FRONT @@ -6260,6 +6423,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 +6581,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 +6689,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)) { @@ -7263,9 +7444,10 @@ public class Intent implements Parcelable, Cloneable { // Since we migrated in child, we need to promote ClipData // and flags to ourselves to grant. setClipData(target.getClipData()); - addFlags(target.getFlags() - & (FLAG_GRANT_READ_URI_PERMISSION | FLAG_GRANT_WRITE_URI_PERMISSION - | FLAG_GRANT_PERSISTABLE_URI_PERMISSION)); + addFlags(target.getFlags() & (FLAG_GRANT_READ_URI_PERMISSION + | FLAG_GRANT_WRITE_URI_PERMISSION + | FLAG_GRANT_PERSISTABLE_URI_PERMISSION + | FLAG_GRANT_PREFIX_URI_PERMISSION)); return true; } else { return false; @@ -7339,4 +7521,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/Task.java b/core/java/android/content/Task.java new file mode 100644 index 0000000..ed5ed88 --- /dev/null +++ b/core/java/android/content/Task.java @@ -0,0 +1,400 @@ +/* + * 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; + +import android.app.task.TaskService; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Container of data passed to the {@link android.content.TaskManager} fully encapsulating the + * parameters required to schedule work against the calling application. These are constructed + * using the {@link Task.Builder}. + */ +public class Task implements Parcelable { + + public interface NetworkType { + public final int ANY = 0; + public final int UNMETERED = 1; + } + + /** + * Linear: retry_time(failure_time, t) = failure_time + initial_retry_delay * t, t >= 1 + * Expon: retry_time(failure_time, t) = failure_time + initial_retry_delay ^ t, t >= 1 + */ + public interface BackoffPolicy { + public final int LINEAR = 0; + public final int EXPONENTIAL = 1; + } + + /** + * Unique task id associated with this class. This is assigned to your task by the scheduler. + */ + public int getTaskId() { + return taskId; + } + + /** + * Bundle of extras which are returned to your application at execution time. + */ + public Bundle getExtras() { + return extras; + } + + /** + * Name of the service endpoint that will be called back into by the TaskManager. + */ + public String getServiceClassName() { + return serviceClassName; + } + + /** + * Whether this task needs the device to be plugged in. + */ + public boolean isRequireCharging() { + return requireCharging; + } + + /** + * Whether this task needs the device to be in an Idle maintenance window. + */ + public boolean isRequireDeviceIdle() { + return requireDeviceIdle; + } + + /** + * See {@link android.content.Task.NetworkType} for a description of this value. + */ + public int getNetworkCapabilities() { + return networkCapabilities; + } + + /** + * Set for a task that does not recur periodically, to specify a delay after which the task + * will be eligible for execution. This value is not set if the task recurs periodically. + */ + public long getMinLatencyMillis() { + return minLatencyMillis; + } + + /** + * See {@link Builder#setOverrideDeadline(long)}. This value is not set if the task recurs + * periodically. + */ + public long getMaxExecutionDelayMillis() { + return maxExecutionDelayMillis; + } + + /** + * Track whether this task will repeat with a given period. + */ + public boolean isPeriodic() { + return isPeriodic; + } + + /** + * Set to the interval between occurrences of this task. This value is <b>not</b> set if the + * task does not recur periodically. + */ + public long getIntervalMillis() { + return intervalMillis; + } + + /** + * The amount of time the TaskManager will wait before rescheduling a failed task. This value + * will be increased depending on the backoff policy specified at task creation time. Defaults + * to 5 seconds. + */ + public long getInitialBackoffMillis() { + return initialBackoffMillis; + } + + /** + * See {@link android.content.Task.BackoffPolicy} for an explanation of the values this field + * can take. This defaults to exponential. + */ + public int getBackoffPolicy() { + return backoffPolicy; + } + + private final int taskId; + // TODO: Change this to use PersistableBundle when that lands in master. + private final Bundle extras; + private final String serviceClassName; + private final boolean requireCharging; + private final boolean requireDeviceIdle; + private final int networkCapabilities; + private final long minLatencyMillis; + private final long maxExecutionDelayMillis; + private final boolean isPeriodic; + private final long intervalMillis; + private final long initialBackoffMillis; + private final int backoffPolicy; + + private Task(Parcel in) { + taskId = in.readInt(); + extras = in.readBundle(); + serviceClassName = in.readString(); + requireCharging = in.readInt() == 1; + requireDeviceIdle = in.readInt() == 1; + networkCapabilities = in.readInt(); + minLatencyMillis = in.readLong(); + maxExecutionDelayMillis = in.readLong(); + isPeriodic = in.readInt() == 1; + intervalMillis = in.readLong(); + initialBackoffMillis = in.readLong(); + backoffPolicy = in.readInt(); + } + + private Task(Task.Builder b) { + taskId = b.mTaskId; + extras = new Bundle(b.mExtras); + serviceClassName = b.mTaskServiceClassName; + requireCharging = b.mRequiresCharging; + requireDeviceIdle = b.mRequiresDeviceIdle; + networkCapabilities = b.mNetworkCapabilities; + minLatencyMillis = b.mMinLatencyMillis; + maxExecutionDelayMillis = b.mMaxExecutionDelayMillis; + isPeriodic = b.mIsPeriodic; + intervalMillis = b.mIntervalMillis; + initialBackoffMillis = b.mInitialBackoffMillis; + backoffPolicy = b.mBackoffPolicy; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(taskId); + out.writeBundle(extras); + out.writeString(serviceClassName); + out.writeInt(requireCharging ? 1 : 0); + out.writeInt(requireDeviceIdle ? 1 : 0); + out.writeInt(networkCapabilities); + out.writeLong(minLatencyMillis); + out.writeLong(maxExecutionDelayMillis); + out.writeInt(isPeriodic ? 1 : 0); + out.writeLong(intervalMillis); + out.writeLong(initialBackoffMillis); + out.writeInt(backoffPolicy); + } + + public static final Creator<Task> CREATOR = new Creator<Task>() { + @Override + public Task createFromParcel(Parcel in) { + return new Task(in); + } + + @Override + public Task[] newArray(int size) { + return new Task[size]; + } + }; + + /** + * Builder class for constructing {@link Task} objects. + */ + public final class Builder { + private int mTaskId; + private Bundle mExtras; + private String mTaskServiceClassName; + // Requirements. + private boolean mRequiresCharging; + private boolean mRequiresDeviceIdle; + private int mNetworkCapabilities; + // One-off parameters. + private long mMinLatencyMillis; + private long mMaxExecutionDelayMillis; + // Periodic parameters. + private boolean mIsPeriodic; + private long mIntervalMillis; + // Back-off parameters. + private long mInitialBackoffMillis = 5000L; + private int mBackoffPolicy = BackoffPolicy.EXPONENTIAL; + /** Easy way to track whether the client has tried to set a back-off policy. */ + private boolean mBackoffPolicySet = false; + + /** + * @param taskId Application-provided id for this task. Subsequent calls to cancel, or + * tasks created with the same taskId, will update the pre-existing task with + * the same id. + * @param cls The endpoint that you implement that will receive the callback from the + * TaskManager. + */ + public Builder(int taskId, Class<TaskService> cls) { + mTaskServiceClassName = cls.getClass().getName(); + mTaskId = taskId; + } + + /** + * Set optional extras. This is persisted, so we only allow primitive types. + * @param extras Bundle containing extras you want the scheduler to hold on to for you. + */ + public Builder setExtras(Bundle extras) { + mExtras = extras; + return this; + } + + /** + * Set some description of the kind of network capabilities you would like to have. This + * will be a parameter defined in {@link android.content.Task.NetworkType}. + * Not calling this function means the network is not necessary. + * Bear in mind that calling this function defines network as a strict requirement for your + * task if the network requested is not available your task will never run. See + * {@link #setOverrideDeadline(long)} to change this behaviour. + */ + public Builder setRequiredNetworkCapabilities(int networkCapabilities) { + mNetworkCapabilities = networkCapabilities; + return this; + } + + /* + * Specify that to run this task, the device needs to be plugged in. This defaults to + * false. + * @param requireCharging Whether or not the device is plugged in. + */ + public Builder setRequiresCharging(boolean requiresCharging) { + mRequiresCharging = requiresCharging; + return this; + } + + /** + * Specify that to run, the task needs the device to be in idle mode. This defaults to + * false. + * <p>Idle mode is a loose definition provided by the system, which means that the device + * is not in use, and has not been in use for some time. As such, it is a good time to + * perform resource heavy tasks. Bear in mind that battery usage will still be attributed + * to your application, and surfaced to the user in battery stats.</p> + * @param requiresDeviceIdle Whether or not the device need be within an idle maintenance + * window. + */ + public Builder setRequiresDeviceIdle(boolean requiresDeviceIdle) { + mRequiresDeviceIdle = requiresDeviceIdle; + return this; + } + + /** + * Specify that this task should recur with the provided interval, not more than once per + * period. You have no control over when within this interval this task will be executed, + * only the guarantee that it will be executed at most once within this interval. + * A periodic task will be repeated until the phone is turned off, however it will only be + * persisted if the client app has declared the + * {@link android.Manifest.permission#RECEIVE_BOOT_COMPLETED} permission. You can schedule + * periodic tasks without this permission, they simply will cease to exist after the phone + * restarts. + * Setting this function on the builder with {@link #setMinimumLatency(long)} or + * {@link #setOverrideDeadline(long)} will result in an error. + * @param intervalMillis Millisecond interval for which this task will repeat. + */ + public Builder setPeriodic(long intervalMillis) { + mIsPeriodic = true; + mIntervalMillis = intervalMillis; + return this; + } + + /** + * Specify that this task should be delayed by the provided amount of time. + * Because it doesn't make sense setting this property on a periodic task, doing so will + * throw an {@link java.lang.IllegalArgumentException} when + * {@link android.content.Task.Builder#build()} is called. + * @param minLatencyMillis Milliseconds before which this task will not be considered for + * execution. + */ + public Builder setMinimumLatency(long minLatencyMillis) { + mMinLatencyMillis = minLatencyMillis; + return this; + } + + /** + * Set deadline which is the maximum scheduling latency. The task will be run by this + * deadline even if other requirements are not met. Because it doesn't make sense setting + * this property on a periodic task, doing so will throw an + * {@link java.lang.IllegalArgumentException} when + * {@link android.content.Task.Builder#build()} is called. + */ + public Builder setOverrideDeadline(long maxExecutionDelayMillis) { + mMaxExecutionDelayMillis = maxExecutionDelayMillis; + return this; + } + + /** + * Set up the back-off/retry policy. + * This defaults to some respectable values: {5 seconds, Exponential}. We cap back-off at + * 1hr. + * Note that trying to set a backoff criteria for a task with + * {@link #setRequiresDeviceIdle(boolean)} will throw an exception when you call build(). + * This is because back-off typically does not make sense for these types of tasks. See + * {@link android.app.task.TaskService#taskFinished(android.app.task.TaskParams, boolean)} + * for more description of the return value for the case of a task executing while in idle + * mode. + * @param initialBackoffMillis Millisecond time interval to wait initially when task has + * failed. + * @param backoffPolicy is one of {@link BackoffPolicy} + */ + public Builder setBackoffCriteria(long initialBackoffMillis, int backoffPolicy) { + mBackoffPolicySet = true; + mInitialBackoffMillis = initialBackoffMillis; + mBackoffPolicy = backoffPolicy; + return this; + } + + /** + * @return The task object to hand to the TaskManager. This object is immutable. + */ + public Task build() { + // Check that extras bundle only contains primitive types. + try { + for (String key : extras.keySet()) { + Object value = extras.get(key); + if (value == null) continue; + if (value instanceof Long) continue; + if (value instanceof Integer) continue; + if (value instanceof Boolean) continue; + if (value instanceof Float) continue; + if (value instanceof Double) continue; + if (value instanceof String) continue; + throw new IllegalArgumentException("Unexpected value type: " + + value.getClass().getName()); + } + } catch (IllegalArgumentException e) { + throw e; + } catch (RuntimeException exc) { + throw new IllegalArgumentException("error unparcelling Bundle", exc); + } + // Check that a deadline was not set on a periodic task. + if (mIsPeriodic && (mMaxExecutionDelayMillis != 0L)) { + throw new IllegalArgumentException("Can't call setOverrideDeadline() on a " + + "periodic task."); + } + if (mIsPeriodic && (mMinLatencyMillis != 0L)) { + throw new IllegalArgumentException("Can't call setMinimumLatency() on a " + + "periodic task"); + } + if (mBackoffPolicySet && mRequiresDeviceIdle) { + throw new IllegalArgumentException("An idle mode task will not respect any" + + " back-off policy, so calling setBackoffCriteria with" + + " setRequiresDeviceIdle is an error."); + } + return new Task(this); + } + } + +} diff --git a/core/java/android/content/TaskManager.java b/core/java/android/content/TaskManager.java new file mode 100644 index 0000000..d28d78a --- /dev/null +++ b/core/java/android/content/TaskManager.java @@ -0,0 +1,66 @@ +/* + * 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; + +import java.util.List; + +/** + * Class for scheduling various types of tasks with the scheduling framework on the device. + * + * Get an instance of this class through {@link Context#getSystemService(String)}. + */ +public abstract class TaskManager { + /* + * Returned from {@link #schedule(Task)} when an invalid parameter was supplied. This can occur + * if the run-time for your task is too short, or perhaps the system can't resolve the + * requisite {@link TaskService} in your package. + */ + static final int RESULT_INVALID_PARAMETERS = -1; + /** + * Returned from {@link #schedule(Task)} if this application has made too many requests for + * work over too short a time. + */ + // TODO: Determine if this is necessary. + static final int RESULT_OVER_QUOTA = -2; + + /* + * @param task The task you wish scheduled. See {@link Task#TaskBuilder} for more detail on + * the sorts of tasks you can schedule. + * @return If >0, this int corresponds to the taskId of the successfully scheduled task. + * Otherwise you have to compare the return value to the error codes defined in this class. + */ + public abstract int schedule(Task task); + + /** + * Cancel a task that is pending in the TaskManager. + * @param taskId unique identifier for this task. Obtain this value from the tasks returned by + * {@link #getAllPendingTasks()}. + * @return + */ + public abstract void cancel(int taskId); + + /** + * Cancel all tasks that have been registered with the TaskManager by this package. + */ + public abstract void cancelAll(); + + /** + * @return a list of all the tasks registered by this package that have not yet been executed. + */ + public abstract List<Task> getAllPendingTasks(); + +} diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index 941b726..c53e545 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 * android.R.attr#primaryUserOnly attribute. @@ -219,6 +230,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. @@ -330,6 +363,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..0acf043 --- /dev/null +++ b/core/java/android/content/pm/ILauncherApps.aidl @@ -0,0 +1,40 @@ +/** + * 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); + boolean isPackageEnabled(String packageName, in UserHandle user); + boolean isActivityEnabled(in ComponentName component, 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..cf9a296 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; @@ -73,6 +74,9 @@ interface IPackageManager { ActivityInfo getActivityInfo(in ComponentName className, int flags, int userId); + boolean activitySupportsIntent(in ComponentName className, in Intent intent, + String resolvedType); + ActivityInfo getReceiverInfo(in ComponentName className, int flags, int userId); ServiceInfo getServiceInfo(in ComponentName className, int flags, int userId); @@ -107,6 +111,8 @@ interface IPackageManager { ResolveInfo resolveIntent(in Intent intent, String resolvedType, int flags, int userId); + boolean canForwardTo(in Intent intent, String resolvedType, int userIdFrom, int userIdDest); + List<ResolveInfo> queryIntentActivities(in Intent intent, String resolvedType, int flags, int userId); @@ -237,6 +243,14 @@ 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); + + void addForwardingIntentFilter(in IntentFilter filter, int userIdOrig, int userIdDest); + + void clearForwardingIntentFilters(int userIdOrig); + /** * Report the set of 'Home' activity candidates, plus (if any) which of them * is the current "always use this one" setting. @@ -402,6 +416,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..9087338 --- /dev/null +++ b/core/java/android/content/pm/LauncherActivityInfo.java @@ -0,0 +1,167 @@ +/* + * 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.PackageManager.NameNotFoundException; +import android.content.res.Resources; +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 static final String TAG = "LauncherActivityInfo"; + + 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 name for the acitivty from android:name in the manifest. + * @return the name from android:name for the acitivity. + */ + public String getName() { + return mActivityInfo.name; + } + + /** + * 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) { + int iconRes = mActivityInfo.getIconResource(); + Resources resources = null; + Drawable originalIcon = null; + try { + resources = mPm.getResourcesForApplication(mActivityInfo.applicationInfo); + try { + if (density != 0) { + originalIcon = resources.getDrawableForDensity(iconRes, density); + } + } catch (Resources.NotFoundException e) { + } + } catch (NameNotFoundException nnfe) { + } + + if (originalIcon == null) { + originalIcon = mActivityInfo.loadIcon(mPm); + } + + if (originalIcon instanceof BitmapDrawable) { + return mUm.getBadgedDrawableForUser( + originalIcon, mUser); + } else { + Log.e(TAG, "Unable to create badged icon for " + mActivityInfo); + } + 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..8025b60 --- /dev/null +++ b/core/java/android/content/pm/LauncherApps.java @@ -0,0 +1,319 @@ +/* + * 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! + } + } + + /** + * Checks if the package is installed and enabled for a profile. + * + * @param packageName The package to check. + * @param user The UserHandle of the profile. + * + * @return true if the package exists and is enabled. + */ + public boolean isPackageEnabledForProfile(String packageName, UserHandle user) { + try { + return mService.isPackageEnabled(packageName, user); + } catch (RemoteException re) { + return false; + } + } + + /** + * Checks if the activity exists and it enabled for a profile. + * + * @param component The activity to check. + * @param user The UserHandle of the profile. + * + * @return true if the activity exists and is enabled. + */ + public boolean isActivityEnabledForProfile(ComponentName component, UserHandle user) { + try { + return mService.isActivityEnabled(component, user); + } catch (RemoteException re) { + return false; + } + } + + + /** + * 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..484a2a1 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 @@ -1212,7 +1235,6 @@ public abstract class PackageManager { */ @SdkConstant(SdkConstantType.FEATURE) public static final String FEATURE_LIVE_WALLPAPER = "android.software.live_wallpaper"; - /** * Feature for {@link #getSystemAvailableFeatures} and * {@link #hasSystemFeature}: The device supports app widgets. @@ -1221,6 +1243,17 @@ public abstract class PackageManager { public static final String FEATURE_APP_WIDGETS = "android.software.app_widgets"; /** + * @hide + * Feature for {@link #getSystemAvailableFeatures} and + * {@link #hasSystemFeature}: The device supports + * {@link android.service.voice.VoiceInteractionService} and + * {@link android.app.VoiceInteractor}. + */ + @SdkConstant(SdkConstantType.FEATURE) + public static final String FEATURE_VOICE_RECOGNIZERS = "android.software.voice_recognizers"; + + + /** * Feature for {@link #getSystemAvailableFeatures} and * {@link #hasSystemFeature}: The device supports a home screen that is replaceable * by third party applications. @@ -1314,6 +1347,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 +1443,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 +2839,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 +2862,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 +2877,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 +2897,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 +2910,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 66d4f50..080b37b 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -50,6 +50,7 @@ import java.security.spec.EncodedKeySpec; import java.security.spec.InvalidKeySpecException; import java.security.spec.X509EncodedKeySpec; import java.util.ArrayList; +import java.util.Arrays; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; @@ -57,7 +58,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; @@ -148,8 +148,7 @@ public class PackageParser { private String[] mSeparateProcesses; private boolean mOnlyCoreApps; private static final int SDK_VERSION = Build.VERSION.SDK_INT; - private static final String SDK_CODENAME = "REL".equals(Build.VERSION.CODENAME) - ? null : Build.VERSION.CODENAME; + private static final String[] SDK_CODENAMES = Build.VERSION.ACTIVE_CODENAMES; private int mParseError = PackageManager.INSTALL_SUCCEEDED; @@ -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; @@ -459,7 +459,7 @@ public class PackageParser { return pi; } - private Certificate[] loadCertificates(StrictJarFile jarFile, ZipEntry je, + private Certificate[][] loadCertificates(StrictJarFile jarFile, ZipEntry je, byte[] readBuffer) { try { // We must read the stream for the JarEntry to retrieve @@ -469,7 +469,7 @@ public class PackageParser { // not using } is.close(); - return je != null ? jarFile.getCertificates(je) : null; + return je != null ? jarFile.getCertificateChains(je) : null; } catch (IOException e) { Slog.w(TAG, "Exception reading " + je.getName() + " in " + jarFile, e); } catch (RuntimeException e) { @@ -632,7 +632,7 @@ public class PackageParser { try { StrictJarFile jarFile = new StrictJarFile(mArchiveSourcePath); - Certificate[] certs = null; + Certificate[][] certs = null; if ((flags&PARSE_IS_SYSTEM) != 0) { // If this package comes from the system image, then we @@ -656,8 +656,8 @@ public class PackageParser { final int N = certs.length; for (int i=0; i<N; i++) { Slog.i(TAG, " Public key: " - + certs[i].getPublicKey().getEncoded() - + " " + certs[i].getPublicKey()); + + certs[i][0].getPublicKey().getEncoded() + + " " + certs[i][0].getPublicKey()); } } } @@ -677,7 +677,7 @@ public class PackageParser { ManifestDigest.fromInputStream(jarFile.getInputStream(je)); } - final Certificate[] localCerts = loadCertificates(jarFile, je, readBuffer); + final Certificate[][] localCerts = loadCertificates(jarFile, je, readBuffer); if (DEBUG_JAR) { Slog.i(TAG, "File " + mArchiveSourcePath + " entry " + je.getName() + ": certs=" + certs + " (" @@ -726,8 +726,7 @@ public class PackageParser { final int N = certs.length; pkg.mSignatures = new Signature[certs.length]; for (int i=0; i<N; i++) { - pkg.mSignatures[i] = new Signature( - certs[i].getEncoded()); + pkg.mSignatures[i] = new Signature(certs[i]); } } else { Slog.e(TAG, "Package " + pkg.packageName @@ -739,7 +738,7 @@ public class PackageParser { // Add the signing KeySet to the system pkg.mSigningKeys = new HashSet<PublicKey>(); for (int i=0; i < certs.length; i++) { - pkg.mSigningKeys.add(certs[i].getPublicKey()); + pkg.mSigningKeys.add(certs[i][0].getPublicKey()); } } catch (CertificateEncodingException e) { @@ -988,7 +987,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 +1008,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 +1104,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, @@ -1200,10 +1199,18 @@ public class PackageParser { sa.recycle(); if (minCode != null) { - if (!minCode.equals(SDK_CODENAME)) { - if (SDK_CODENAME != null) { + boolean allowedCodename = false; + for (String codename : SDK_CODENAMES) { + if (minCode.equals(codename)) { + allowedCodename = true; + break; + } + } + if (!allowedCodename) { + if (SDK_CODENAMES.length > 0) { outError[0] = "Requires development platform " + minCode - + " (current platform is " + SDK_CODENAME + ")"; + + " (current platform is any of " + + Arrays.toString(SDK_CODENAMES) + ")"; } else { outError[0] = "Requires development platform " + minCode + " but this is a release platform."; @@ -1219,10 +1226,18 @@ public class PackageParser { } if (targetCode != null) { - if (!targetCode.equals(SDK_CODENAME)) { - if (SDK_CODENAME != null) { + boolean allowedCodename = false; + for (String codename : SDK_CODENAMES) { + if (targetCode.equals(codename)) { + allowedCodename = true; + break; + } + } + if (!allowedCodename) { + if (SDK_CODENAMES.length > 0) { outError[0] = "Requires development platform " + targetCode - + " (current platform is " + SDK_CODENAME + ")"; + + " (current platform is any of " + + Arrays.toString(SDK_CODENAMES) + ")"; } else { outError[0] = "Requires development platform " + targetCode + " but this is a release platform."; @@ -1986,6 +2001,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); @@ -2446,6 +2463,11 @@ public class PackageParser { } if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestActivity_persistable, false)) { + a.info.flags |= ActivityInfo.FLAG_PERSISTABLE; + } + + if (sa.getBoolean( com.android.internal.R.styleable.AndroidManifestActivity_allowEmbedded, false)) { a.info.flags |= ActivityInfo.FLAG_ALLOW_EMBEDDED; @@ -3291,19 +3313,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); @@ -3597,6 +3623,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/Signature.java b/core/java/android/content/pm/Signature.java index 752bf8b..f4e7dc3 100644 --- a/core/java/android/content/pm/Signature.java +++ b/core/java/android/content/pm/Signature.java @@ -25,6 +25,7 @@ import java.io.ByteArrayInputStream; import java.lang.ref.SoftReference; import java.security.PublicKey; import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.util.Arrays; @@ -38,12 +39,28 @@ public class Signature implements Parcelable { private int mHashCode; private boolean mHaveHashCode; private SoftReference<String> mStringRef; + private Certificate[] mCertificateChain; /** * Create Signature from an existing raw byte array. */ public Signature(byte[] signature) { mSignature = signature.clone(); + mCertificateChain = null; + } + + /** + * Create signature from a certificate chain. Used for backward + * compatibility. + * + * @throws CertificateEncodingException + * @hide + */ + public Signature(Certificate[] certificateChain) throws CertificateEncodingException { + mSignature = certificateChain[0].getEncoded(); + if (certificateChain.length > 1) { + mCertificateChain = Arrays.copyOfRange(certificateChain, 1, certificateChain.length); + } } private static final int parseHexDigit(int nibble) { @@ -156,6 +173,29 @@ public class Signature implements Parcelable { return cert.getPublicKey(); } + /** + * Used for compatibility code that needs to check the certificate chain + * during upgrades. + * + * @throws CertificateEncodingException + * @hide + */ + public Signature[] getChainSignatures() throws CertificateEncodingException { + if (mCertificateChain == null) { + return new Signature[] { this }; + } + + Signature[] chain = new Signature[1 + mCertificateChain.length]; + chain[0] = this; + + int i = 1; + for (Certificate c : mCertificateChain) { + chain[i++] = new Signature(c.getEncoded()); + } + + return chain; + } + @Override public boolean equals(Object obj) { try { diff --git a/core/java/android/content/pm/UserInfo.java b/core/java/android/content/pm/UserInfo.java index 4c87830..c0383a3 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; /** @@ -26,8 +27,8 @@ import android.os.UserHandle; */ public class UserInfo implements Parcelable { - /** 6 bits for user type */ - public static final int FLAG_MASK_USER_TYPE = 0x0000003F; + /** 8 bits for user type */ + public static final int FLAG_MASK_USER_TYPE = 0x000000FF; /** * *************************** NOTE *************************** @@ -63,6 +64,20 @@ 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; + + /** + * Indicates that this user is disabled. + */ + public static final int FLAG_DISABLED = 0x00000040; + + + public static final int NO_PROFILE_GROUP_ID = -1; + public int id; public int serialNumber; public String name; @@ -70,6 +85,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 +99,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 +118,22 @@ public class UserInfo implements Parcelable { return (flags & FLAG_RESTRICTED) == FLAG_RESTRICTED; } + public boolean isManagedProfile() { + return (flags & FLAG_MANAGED_PROFILE) == FLAG_MANAGED_PROFILE; + } + + public boolean isEnabled() { + return (flags & FLAG_DISABLED) != FLAG_DISABLED; + } + + /** + * @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 +146,7 @@ public class UserInfo implements Parcelable { creationTime = orig.creationTime; lastLoggedInTime = orig.lastLoggedInTime; partial = orig.partial; + profileGroupId = orig.profileGroupId; } public UserHandle getUserHandle() { @@ -137,6 +171,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 +193,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 7318652..1331777 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,86 +72,112 @@ 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 */ - public static int selectDefaultTheme(int curTheme, int targetSdkVersion) { + /** + * Returns the most appropriate default theme for the specified target SDK version. + * + * @param curTheme The current theme, or 0 if not specified. + * @param targetSdkVersion The target SDK version. + * @return A theme resource identifier + * @hide + */ + public 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.array.system_theme_sdks, + com.android.internal.R.array.system_theme_styles); } - - /** @hide */ - public static int selectSystemTheme(int curTheme, int targetSdkVersion, - int orig, int holo, int deviceDefault) { + + /** + * Returns the most appropriate default theme for the specified target SDK version. + * + * @param curTheme The current theme, or 0 if not specified. + * @param targetSdkVersion The target SDK version. + * @param sdkArrayId Identifier for integer array resource containing + * sorted minimum SDK versions. First entry must be 0. + * @param themeArrayId Identifier for array resource containing the + * default themes that map to SDK versions. + * @return A theme resource identifier + * @hide + */ + public int selectSystemTheme( + int curTheme, int targetSdkVersion, int sdkArrayId, int themeArrayId) { if (curTheme != 0) { return curTheme; } - if (targetSdkVersion < Build.VERSION_CODES.HONEYCOMB) { - return orig; - } - if (targetSdkVersion < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { - return holo; + + final int[] targetSdks = getIntArray(sdkArrayId); + final TypedArray defaultThemes = obtainTypedArray(themeArrayId); + for (int i = targetSdks.length - 1; i > 0; i--) { + if (targetSdkVersion >= targetSdks[i]) { + return defaultThemes.getResourceId(i, 0); + } } - return deviceDefault; + + return defaultThemes.getResourceId(0, 0); } - + /** * This exception is thrown by the resource APIs when a requested resource * can not be found. @@ -513,7 +539,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 +707,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 +738,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 +756,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 +812,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 +1271,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 +1288,8 @@ public class Resources { */ public void setTo(Theme other) { AssetManager.copyTheme(mTheme, other.mTheme); + + mThemeResId = other.mThemeResId; } /** @@ -1246,11 +1312,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 +1339,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 +1369,7 @@ public class Resources { } System.out.println(s); } + AssetManager.applyStyle(mTheme, 0, resid, 0, attrs, array.mData, array.mIndices); return array; } @@ -1361,20 +1423,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 +1470,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 +1525,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 +1537,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 +1570,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 +1582,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*/ long getNativeTheme() { return mTheme; } + + /*package*/ int getAppliedStyleResId() { + return mThemeResId; + } } /** @@ -1492,7 +1623,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 +1633,6 @@ public class Resources { mAssets.retrieveAttributes(parser.mParseState, attrs, array.mData, array.mIndices); - array.mRsrcs = attrs; array.mXml = parser; return array; @@ -1574,7 +1704,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 +1737,8 @@ public class Resources { + " final compat is " + mCompatibilityInfo); } - clearDrawableCacheLocked(mDrawableCache, configChanges); - clearDrawableCacheLocked(mColorDrawableCache, configChanges); + clearDrawableCachesLocked(mDrawableCache, configChanges); + clearDrawableCachesLocked(mColorDrawableCache, configChanges); mColorStateListCache.clear(); @@ -1621,18 +1751,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 +1792,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 +2166,7 @@ public class Resources { /** * @hide */ - public LongSparseArray<Drawable.ConstantState> getPreloadedDrawables() { + public LongSparseArray<ConstantState> getPreloadedDrawables() { return sPreloadedDrawables[0]; } @@ -2006,6 +2184,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 +2205,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); + } + + return dr; + } - String file = value.string.toString(); + 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 \"" + getResourceName(id) + "\" (" + + Integer.toHexString(id) + ") 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 +2449,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 +2577,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 +2596,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..722d956 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<Integer> CONTROL_MAX_REGIONS = - new Key<Integer>("android.control.maxRegions", int.class); + public static final Key<byte[]> EDGE_AVAILABLE_EDGE_MODES = + new Key<byte[]>("android.edge.availableEdgeModes", byte[].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>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<Byte> FLASH_INFO_AVAILABLE = - new Key<Byte>("android.flash.info.available", byte.class); + public static final Key<Boolean> FLASH_INFO_AVAILABLE = + new Key<Boolean>("android.flash.info.available", boolean.class); /** - * <p> - * Supported resolutions for the JPEG - * thumbnail - * </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[]> HOT_PIXEL_AVAILABLE_HOT_PIXEL_MODES = + new Key<byte[]>("android.hotPixel.availableHotPixelModes", byte[].class); + + /** + * <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,1011 @@ 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 (but not stalling), and processed (and stalling) + * formats respectively. For example, assuming that JPEG is typically a processed and + * stalling stream, 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_STREAM_CONFIGURATIONS android.scaler.availableStreamConfigurations}. + * The formats defined in {@link CameraCharacteristics#SCALER_AVAILABLE_STREAM_CONFIGURATIONS android.scaler.availableStreamConfigurations} can be catergorized + * into the 3 stream types as below:</p> + * <ul> + * <li>Processed (but stalling): any non-RAW format with a stallDurations > 0. + * Typically JPEG format (ImageFormat#JPEG).</li> + * <li>Raw formats: ImageFormat#RAW_SENSOR and ImageFormat#RAW_OPAQUE.</li> + * <li>Processed (but not-stalling): any non-RAW format without a stall duration. + * Typically ImageFormat#YUV_420_888, ImageFormat#NV21, ImageFormat#YV12.</li> + * </ul> + * + * @see CameraCharacteristics#SCALER_AVAILABLE_STREAM_CONFIGURATIONS */ 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> + * + * @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>The standard reference illuminant used as the scene light source when + * calculating the {@link CameraCharacteristics#SENSOR_COLOR_TRANSFORM1 android.sensor.colorTransform1}, + * {@link CameraCharacteristics#SENSOR_CALIBRATION_TRANSFORM1 android.sensor.calibrationTransform1}, and + * {@link CameraCharacteristics#SENSOR_FORWARD_MATRIX1 android.sensor.forwardMatrix1} matrices.</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 calibrating camera devices.</p> + * <p>If this tag is present, then {@link CameraCharacteristics#SENSOR_COLOR_TRANSFORM1 android.sensor.colorTransform1}, + * {@link CameraCharacteristics#SENSOR_CALIBRATION_TRANSFORM1 android.sensor.calibrationTransform1}, and + * {@link CameraCharacteristics#SENSOR_FORWARD_MATRIX1 android.sensor.forwardMatrix1} will also be present.</p> + * <p>Some devices may choose to provide a second set of calibration + * information for improved quality, including + * {@link CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT2 android.sensor.referenceIlluminant2} and its corresponding matrices.</p> + * + * @see CameraCharacteristics#SENSOR_CALIBRATION_TRANSFORM1 + * @see CameraCharacteristics#SENSOR_COLOR_TRANSFORM1 + * @see CameraCharacteristics#SENSOR_FORWARD_MATRIX1 + * @see CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT2 + * @see #SENSOR_REFERENCE_ILLUMINANT1_DAYLIGHT + * @see #SENSOR_REFERENCE_ILLUMINANT1_FLUORESCENT + * @see #SENSOR_REFERENCE_ILLUMINANT1_TUNGSTEN + * @see #SENSOR_REFERENCE_ILLUMINANT1_FLASH + * @see #SENSOR_REFERENCE_ILLUMINANT1_FINE_WEATHER + * @see #SENSOR_REFERENCE_ILLUMINANT1_CLOUDY_WEATHER + * @see #SENSOR_REFERENCE_ILLUMINANT1_SHADE + * @see #SENSOR_REFERENCE_ILLUMINANT1_DAYLIGHT_FLUORESCENT + * @see #SENSOR_REFERENCE_ILLUMINANT1_DAY_WHITE_FLUORESCENT + * @see #SENSOR_REFERENCE_ILLUMINANT1_COOL_WHITE_FLUORESCENT + * @see #SENSOR_REFERENCE_ILLUMINANT1_WHITE_FLUORESCENT + * @see #SENSOR_REFERENCE_ILLUMINANT1_STANDARD_A + * @see #SENSOR_REFERENCE_ILLUMINANT1_STANDARD_B + * @see #SENSOR_REFERENCE_ILLUMINANT1_STANDARD_C + * @see #SENSOR_REFERENCE_ILLUMINANT1_D55 + * @see #SENSOR_REFERENCE_ILLUMINANT1_D65 + * @see #SENSOR_REFERENCE_ILLUMINANT1_D75 + * @see #SENSOR_REFERENCE_ILLUMINANT1_D50 + * @see #SENSOR_REFERENCE_ILLUMINANT1_ISO_STUDIO_TUNGSTEN + */ + public static final Key<Integer> SENSOR_REFERENCE_ILLUMINANT1 = + new Key<Integer>("android.sensor.referenceIlluminant1", int.class); + + /** + * <p>The standard reference illuminant used as the scene light source when + * calculating the {@link CameraCharacteristics#SENSOR_COLOR_TRANSFORM2 android.sensor.colorTransform2}, + * {@link CameraCharacteristics#SENSOR_CALIBRATION_TRANSFORM2 android.sensor.calibrationTransform2}, and + * {@link CameraCharacteristics#SENSOR_FORWARD_MATRIX2 android.sensor.forwardMatrix2} matrices.</p> + * <p>See {@link CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT1 android.sensor.referenceIlluminant1} for more details. + * Valid values for this are the same as those given for the first + * reference illuminant.</p> + * <p>If this tag is present, then {@link CameraCharacteristics#SENSOR_COLOR_TRANSFORM2 android.sensor.colorTransform2}, + * {@link CameraCharacteristics#SENSOR_CALIBRATION_TRANSFORM2 android.sensor.calibrationTransform2}, and + * {@link CameraCharacteristics#SENSOR_FORWARD_MATRIX2 android.sensor.forwardMatrix2} will also be present.</p> + * + * @see CameraCharacteristics#SENSOR_CALIBRATION_TRANSFORM2 + * @see CameraCharacteristics#SENSOR_COLOR_TRANSFORM2 + * @see CameraCharacteristics#SENSOR_FORWARD_MATRIX2 + * @see CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT1 + */ + public static final Key<Byte> SENSOR_REFERENCE_ILLUMINANT2 = + new Key<Byte>("android.sensor.referenceIlluminant2", byte.class); + + /** + * <p>A per-device calibration transform matrix that maps from the + * reference sensor colorspace to the actual device sensor colorspace.</p> + * <p>This matrix is used to correct for per-device variations in the + * sensor colorspace, and is used for processing raw buffer data.</p> + * <p>The matrix is expressed as a 3x3 matrix in row-major-order, and + * contains a per-device calibration transform that maps colors + * from reference sensor color space (i.e. the "golden module" + * colorspace) into this camera device's native sensor color + * space under the first reference illuminant + * ({@link CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT1 android.sensor.referenceIlluminant1}).</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + * + * @see CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT1 + */ + public static final Key<Rational[]> SENSOR_CALIBRATION_TRANSFORM1 = + new Key<Rational[]>("android.sensor.calibrationTransform1", Rational[].class); + + /** + * <p>A per-device calibration transform matrix that maps from the + * reference sensor colorspace to the actual device sensor colorspace + * (this is the colorspace of the raw buffer data).</p> + * <p>This matrix is used to correct for per-device variations in the + * sensor colorspace, and is used for processing raw buffer data.</p> + * <p>The matrix is expressed as a 3x3 matrix in row-major-order, and + * contains a per-device calibration transform that maps colors + * from reference sensor color space (i.e. the "golden module" + * colorspace) into this camera device's native sensor color + * space under the second reference illuminant + * ({@link CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT2 android.sensor.referenceIlluminant2}).</p> + * <p>This matrix will only be present if the second reference + * illuminant is present.</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + * + * @see CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT2 + */ + public static final Key<Rational[]> SENSOR_CALIBRATION_TRANSFORM2 = + new Key<Rational[]>("android.sensor.calibrationTransform2", Rational[].class); + + /** + * <p>A matrix that transforms color values from CIE XYZ color space to + * reference sensor color space.</p> + * <p>This matrix is used to convert from the standard CIE XYZ color + * space to the reference sensor colorspace, and is used when processing + * raw buffer data.</p> + * <p>The 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 sensor color space (i.e. the + * "golden module" colorspace) under the first reference illuminant + * ({@link CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT1 android.sensor.referenceIlluminant1}).</p> + * <p>The white points chosen in both the reference sensor color space + * and the CIE XYZ colorspace when calculating this transform will + * match the standard white point for the first reference illuminant + * (i.e. no chromatic adaptation will be applied by this transform).</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + * + * @see CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT1 + */ + public static final Key<Rational[]> SENSOR_COLOR_TRANSFORM1 = + new Key<Rational[]>("android.sensor.colorTransform1", Rational[].class); + + /** + * <p>A matrix that transforms color values from CIE XYZ color space to + * reference sensor color space.</p> + * <p>This matrix is used to convert from the standard CIE XYZ color + * space to the reference sensor colorspace, and is used when processing + * raw buffer data.</p> + * <p>The 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 sensor color space (i.e. the + * "golden module" colorspace) under the second reference illuminant + * ({@link CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT2 android.sensor.referenceIlluminant2}).</p> + * <p>The white points chosen in both the reference sensor color space + * and the CIE XYZ colorspace when calculating this transform will + * match the standard white point for the second reference illuminant + * (i.e. no chromatic adaptation will be applied by this transform).</p> + * <p>This matrix will only be present if the second reference + * illuminant is present.</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + * + * @see CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT2 + */ + public static final Key<Rational[]> SENSOR_COLOR_TRANSFORM2 = + new Key<Rational[]>("android.sensor.colorTransform2", Rational[].class); + + /** + * <p>A matrix that transforms white balanced camera colors from the reference + * sensor colorspace to the CIE XYZ colorspace with a D50 whitepoint.</p> + * <p>This matrix is used to convert to the standard CIE XYZ colorspace, and + * is used when processing raw buffer data.</p> + * <p>This matrix is expressed as a 3x3 matrix in row-major-order, and contains + * a color transform matrix that maps white balanced colors from the + * reference sensor color space to the CIE XYZ color space with a D50 white + * point.</p> + * <p>Under the first reference illuminant ({@link CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT1 android.sensor.referenceIlluminant1}) + * this matrix is chosen so that the standard white point for this reference + * illuminant in the reference sensor colorspace is mapped to D50 in the + * CIE XYZ colorspace.</p> + * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + * + * @see CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT1 + */ + public static final Key<Rational[]> SENSOR_FORWARD_MATRIX1 = + new Key<Rational[]>("android.sensor.forwardMatrix1", Rational[].class); + + /** + * <p>A matrix that transforms white balanced camera colors from the reference + * sensor colorspace to the CIE XYZ colorspace with a D50 whitepoint.</p> + * <p>This matrix is used to convert to the standard CIE XYZ colorspace, and + * is used when processing raw buffer data.</p> + * <p>This matrix is expressed as a 3x3 matrix in row-major-order, and contains + * a color transform matrix that maps white balanced colors from the + * reference sensor color space to the CIE XYZ color space with a D50 white + * point.</p> + * <p>Under the second reference illuminant ({@link CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT2 android.sensor.referenceIlluminant2}) + * this matrix is chosen so that the standard white point for this reference + * illuminant in the reference sensor colorspace is mapped to D50 in the + * CIE XYZ colorspace.</p> + * <p>This matrix will only be present if the second reference + * illuminant is present.</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_REFERENCE_ILLUMINANT2 + */ + public static final Key<Rational[]> SENSOR_FORWARD_MATRIX2 = + new Key<Rational[]>("android.sensor.forwardMatrix2", Rational[].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..bb290af 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. * @@ -597,6 +570,14 @@ public interface CameraDevice extends AutoCloseable { public static abstract class CaptureListener { /** + * This constant is used to indicate that no images were captured for + * the request. + * + * @hide + */ + public static final int NO_FRAMES_CAPTURED = -1; + + /** * This method is called when the camera device has started capturing * the output image for the request, at the beginning of image exposure. * @@ -720,9 +701,12 @@ public interface CameraDevice extends AutoCloseable { * The CameraDevice sending the callback. * @param sequenceId * A sequence ID returned by the {@link #capture} family of functions. - * @param frameNumber + * @param lastFrameNumber * The last frame number (returned by {@link CaptureResult#getFrameNumber} * or {@link CaptureFailure#getFrameNumber}) in the capture sequence. + * The last frame number may be equal to NO_FRAMES_CAPTURED if no images + * were captured for this sequence. This can happen, for example, when a + * repeating request or burst is cleared right after being set. * * @see CaptureResult#getFrameNumber() * @see CaptureFailure#getFrameNumber() @@ -730,7 +714,7 @@ public interface CameraDevice extends AutoCloseable { * @see CaptureFailure#getSequenceId() */ public void onCaptureSequenceCompleted(CameraDevice camera, - int sequenceId, int frameNumber) { + int sequenceId, int lastFrameNumber) { // default empty implementation } } 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..ba8db3a 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,313 @@ 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#SENSOR_REFERENCE_ILLUMINANT1 + // + + /** + * @see CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT1 + */ + public static final int SENSOR_REFERENCE_ILLUMINANT1_DAYLIGHT = 1; + + /** + * @see CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT1 + */ + public static final int SENSOR_REFERENCE_ILLUMINANT1_FLUORESCENT = 2; + + /** + * <p>Incandescent light</p> + * @see CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT1 + */ + public static final int SENSOR_REFERENCE_ILLUMINANT1_TUNGSTEN = 3; + + /** + * @see CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT1 + */ + public static final int SENSOR_REFERENCE_ILLUMINANT1_FLASH = 4; + + /** + * @see CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT1 + */ + public static final int SENSOR_REFERENCE_ILLUMINANT1_FINE_WEATHER = 9; + + /** + * @see CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT1 + */ + public static final int SENSOR_REFERENCE_ILLUMINANT1_CLOUDY_WEATHER = 10; + + /** + * @see CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT1 + */ + public static final int SENSOR_REFERENCE_ILLUMINANT1_SHADE = 11; + + /** + * <p>D 5700 - 7100K</p> + * @see CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT1 + */ + public static final int SENSOR_REFERENCE_ILLUMINANT1_DAYLIGHT_FLUORESCENT = 12; + + /** + * <p>N 4600 - 5400K</p> + * @see CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT1 + */ + public static final int SENSOR_REFERENCE_ILLUMINANT1_DAY_WHITE_FLUORESCENT = 13; + + /** + * <p>W 3900 - 4500K</p> + * @see CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT1 + */ + public static final int SENSOR_REFERENCE_ILLUMINANT1_COOL_WHITE_FLUORESCENT = 14; + + /** + * <p>WW 3200 - 3700K</p> + * @see CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT1 + */ + public static final int SENSOR_REFERENCE_ILLUMINANT1_WHITE_FLUORESCENT = 15; + + /** + * @see CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT1 + */ + public static final int SENSOR_REFERENCE_ILLUMINANT1_STANDARD_A = 17; + + /** + * @see CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT1 + */ + public static final int SENSOR_REFERENCE_ILLUMINANT1_STANDARD_B = 18; + + /** + * @see CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT1 + */ + public static final int SENSOR_REFERENCE_ILLUMINANT1_STANDARD_C = 19; + + /** + * @see CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT1 + */ + public static final int SENSOR_REFERENCE_ILLUMINANT1_D55 = 20; + + /** + * @see CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT1 + */ + public static final int SENSOR_REFERENCE_ILLUMINANT1_D65 = 21; + + /** + * @see CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT1 + */ + public static final int SENSOR_REFERENCE_ILLUMINANT1_D75 = 22; + + /** + * @see CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT1 + */ + public static final int SENSOR_REFERENCE_ILLUMINANT1_D50 = 23; + + /** + * @see CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT1 + */ + public static final int SENSOR_REFERENCE_ILLUMINANT1_ISO_STUDIO_TUNGSTEN = 24; + + // // 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 +586,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 +664,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 +698,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 +774,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 +792,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 +840,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 +863,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 +884,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 +907,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 +999,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 +1127,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 +1299,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 +1323,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 +1400,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 +1556,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 +1588,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 +1623,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 +1677,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 +1742,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,50 +1775,61 @@ 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; @@ -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..d8981c8 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,277 @@ 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> + * <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>Optional</b> - This value may be null on some devices. - * - * <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>The estimated camera neutral color in the native sensor colorspace at + * the time of capture.</p> + * <p>This value gives the neutral color point encoded as an RGB value in the + * native sensor color space. The neutral color point indicates the + * currently estimated white point of the scene illumination. It can be + * used to interpolate between the provided color transforms when + * processing raw sensor data.</p> + * <p>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 +1756,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 +1909,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 +2048,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<Long> SYNC_FRAME_NUMBER = + new Key<Long>("android.sync.frameNumber", long.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..988f8f9 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); @@ -135,7 +157,14 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { mCameraId = cameraId; mDeviceListener = listener; mDeviceHandler = handler; - TAG = String.format("CameraDevice-%s-JV", mCameraId); + + final int MAX_TAG_LEN = 23; + String tag = String.format("CameraDevice-JV-%s", mCameraId); + if (tag.length() > MAX_TAG_LEN) { + tag = tag.substring(0, MAX_TAG_LEN); + } + TAG = tag; + DEBUG = Log.isLoggable(TAG, Log.DEBUG); } @@ -251,22 +280,96 @@ 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); + } + + /** + * This method checks lastFrameNumber returned from ICameraDeviceUser methods for + * starting and stopping repeating request and flushing. + * + * <p>If lastFrameNumber is NO_FRAMES_CAPTURED, it means that the request was never + * sent to HAL. Then onCaptureSequenceCompleted is immediately triggered. + * If lastFrameNumber is non-negative, then the requestId and lastFrameNumber pair + * is added to the list mFrameNumberRequestPairs.</p> + * + * @param requestId the request ID of the current repeating request. + * + * @param lastFrameNumber last frame number returned from binder. + */ + private void checkEarlyTriggerSequenceComplete( + final int requestId, final long lastFrameNumber) { + // lastFrameNumber being equal to NO_FRAMES_CAPTURED means that the request + // was never sent to HAL. Should trigger onCaptureSequenceCompleted immediately. + if (lastFrameNumber == CaptureListener.NO_FRAMES_CAPTURED) { + final CaptureListenerHolder holder; + int index = mCaptureListenerMap.indexOfKey(requestId); + holder = (index >= 0) ? mCaptureListenerMap.valueAt(index) : null; + if (holder != null) { + mCaptureListenerMap.removeAt(index); + if (DEBUG) { + Log.v(TAG, String.format( + "remove holder for requestId %d, " + + "because lastFrame is %d.", + requestId, lastFrameNumber)); + } + } + if (holder != null) { + if (DEBUG) { + Log.v(TAG, "immediately trigger onCaptureSequenceCompleted because" + + " request did not reach HAL"); + } + + Runnable resultDispatch = new Runnable() { + @Override + public void run() { + if (!CameraDevice.this.isClosed()) { + if (DEBUG) { + Log.d(TAG, String.format( + "early trigger sequence complete for request %d", + requestId)); + } + if (lastFrameNumber < Integer.MIN_VALUE + || lastFrameNumber > Integer.MAX_VALUE) { + throw new AssertionError(lastFrameNumber + " cannot be cast to int"); + } + holder.getListener().onCaptureSequenceCompleted( + CameraDevice.this, + requestId, + (int)lastFrameNumber); + } + } + }; + holder.getHandler().post(resultDispatch); + } else { + Log.w(TAG, String.format( + "did not register listener to request %d", + requestId)); + } + } else { + mFrameNumberRequestPairs.add( + new SimpleEntry<Long, Integer>(lastFrameNumber, + requestId)); + } } - 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,21 +386,39 @@ 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 (DEBUG) { + Log.v(TAG, "last frame number " + lastFrameNumberRef.getNumber()); + } } catch (CameraRuntimeException e) { throw e.asChecked(); } catch (RemoteException e) { // impossible return -1; } + if (listener != null) { - mCaptureListenerMap.put(requestId, new CaptureListenerHolder(listener, request, - handler, repeating)); + mCaptureListenerMap.put(requestId, new CaptureListenerHolder(listener, + requestList, handler, repeating)); + } else { + if (DEBUG) { + Log.d(TAG, "Listen for request " + requestId + " is null"); + } } + long lastFrameNumber = lastFrameNumberRef.getNumber(); + if (repeating) { + if (mRepeatingRequestId != REQUEST_ID_NONE) { + checkEarlyTriggerSequenceComplete(mRepeatingRequestId, lastFrameNumber); + } mRepeatingRequestId = requestId; + } else { + mFrameNumberRequestPairs.add( + new SimpleEntry<Long, Integer>(lastFrameNumber, requestId)); } if (mIdle) { @@ -312,18 +433,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 +460,17 @@ 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(); + + checkEarlyTriggerSequenceComplete(requestId, lastFrameNumber); + } catch (CameraRuntimeException e) { throw e.asChecked(); } catch (RemoteException e) { @@ -351,8 +481,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 +499,6 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { } mRepeatingRequestId = REQUEST_ID_NONE; - mRepeatingRequestIdDeletedList.clear(); - mCaptureListenerMap.clear(); } } @@ -382,7 +509,13 @@ 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(); + checkEarlyTriggerSequenceComplete(mRepeatingRequestId, lastFrameNumber); + mRepeatingRequestId = REQUEST_ID_NONE; + } } catch (CameraRuntimeException e) { throw e.asChecked(); } catch (RemoteException e) { @@ -428,18 +561,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 +584,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 +610,117 @@ 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) { + Log.e(TAG, String.format( + "result frame number %d comes out of order, should be %d + 1", + frameNumber, mCompletedFrameNumber)); + } + 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 = mCaptureListenerMap.indexOfKey(requestId); + holder = (index >= 0) ? mCaptureListenerMap.valueAt(index) + : null; + if (holder != null) { + mCaptureListenerMap.removeAt(index); + if (DEBUG) { + Log.v(TAG, String.format( + "remove holder for requestId %d, " + + "because lastFrame %d is <= %d", + requestId, frameNumberRequestPair.getKey(), + completedFrameNumber)); + } + } + } + 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)); + } + + long lastFrameNumber = frameNumberRequestPair.getKey(); + if (lastFrameNumber < Integer.MIN_VALUE + || lastFrameNumber > Integer.MAX_VALUE) { + throw new AssertionError(lastFrameNumber + + " cannot be cast to int"); + } + holder.getListener().onCaptureSequenceCompleted( + CameraDevice.this, + requestId, + (int)lastFrameNumber); + } + } + }; + holder.getHandler().post(resultDispatch); + } + + } + } + } + public class CameraDeviceCallbacks extends ICameraDeviceCallbacks.Stub { // @@ -495,7 +755,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 +770,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 +781,14 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { } CameraDevice.this.mDeviceHandler.post(r); } + + // Fire onCaptureSequenceCompleted + if (DEBUG) { + Log.v(TAG, String.format("got error frame %d", resultExtras.getFrameNumber())); + } + mFrameNumberTracker.updateTracker(resultExtras.getFrameNumber(), /*error*/true); + checkAndFireSequenceComplete(); + } @Override @@ -538,7 +807,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 +828,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,56 +841,46 @@ 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); + Log.v(TAG, "Received result frame " + resultExtras.getFrameNumber() + " for id " + + requestId); } final CaptureListenerHolder holder; - - 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(); - } - } - } + Boolean quirkPartial = result.get(CaptureResult.QUIRKS_PARTIAL_RESULT); + boolean quirkIsPartialResult = (quirkPartial != null && quirkPartial); + // Update tracker (increment counter) when it's not a partial result. + if (!quirkIsPartialResult) { + mFrameNumberTracker.updateTracker(resultExtras.getFrameNumber(), /*error*/false); } // Check if we have a listener for this if (holder == null) { + if (DEBUG) { + Log.d(TAG, + "holder is null, early return at frame " + + resultExtras.getFrameNumber()); + } return; } - if (isClosed()) return; + if (isClosed()) { + if (DEBUG) { + Log.d(TAG, + "camera is closed, early return at frame " + + resultExtras.getFrameNumber()); + } + 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 +915,11 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { } holder.getHandler().post(resultDispatch); + + // Fire onCaptureSequenceCompleted + if (!quirkIsPartialResult) { + 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/hdmi/HdmiCec.java b/core/java/android/hardware/hdmi/HdmiCec.java index 8578a32..eafaed6 100644 --- a/core/java/android/hardware/hdmi/HdmiCec.java +++ b/core/java/android/hardware/hdmi/HdmiCec.java @@ -85,7 +85,7 @@ public final class HdmiCec { public static final int ADDR_RESERVED_2 = 13; /** Logical address for TV other than the one assigned with {@link #ADDR_TV} */ - public static final int ADDR_FREE_USE = 14; + public static final int ADDR_SPECIFIC_USE = 14; /** Logical address for devices to which address cannot be allocated */ public static final int ADDR_UNREGISTERED = 15; @@ -179,6 +179,7 @@ public final class HdmiCec { DEVICE_RECORDER, // ADDR_RECORDER_3 DEVICE_TUNER, // ADDR_TUNER_4 DEVICE_PLAYBACK, // ADDR_PLAYBACK_3 + DEVICE_TV, // ADDR_SPECIFIC_USE }; private static final String[] DEFAULT_NAMES = { @@ -194,6 +195,7 @@ public final class HdmiCec { "Recorder_3", "Tuner_4", "Playback_3", + "Secondary_TV", }; private HdmiCec() { } // Prevents instantiation. @@ -221,9 +223,7 @@ public final class HdmiCec { * @return true if the given address is valid */ public static boolean isValidAddress(int address) { - // TODO: We leave out the address 'free use(14)' for now. Check this later - // again to make sure it is a valid address for communication. - return (ADDR_TV <= address && address <= ADDR_PLAYBACK_3); + return (ADDR_TV <= address && address <= ADDR_SPECIFIC_USE); } /** diff --git a/core/java/android/hardware/hdmi/HdmiCecDeviceInfo.java b/core/java/android/hardware/hdmi/HdmiCecDeviceInfo.java new file mode 100644 index 0000000..9698445 --- /dev/null +++ b/core/java/android/hardware/hdmi/HdmiCecDeviceInfo.java @@ -0,0 +1,168 @@ +/* + * 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.hdmi; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * A class to encapsulate device information for HDMI-CEC. This container + * include basic information such as logical address, physical address and + * device type, and additional information like vendor id and osd name. + */ +public final class HdmiCecDeviceInfo implements Parcelable { + // Logical address, phsical address, device type, vendor id and display name + // are immutable value. + private final int mLogicalAddress; + private final int mPhysicalAddress; + private final int mDeviceType; + private final int mVendorId; + private final String mDisplayName; + + + /** + * A helper class to deserialize {@link HdmiCecDeviceInfo} for a parcel. + */ + public static final Parcelable.Creator<HdmiCecDeviceInfo> CREATOR = + new Parcelable.Creator<HdmiCecDeviceInfo>() { + @Override + public HdmiCecDeviceInfo createFromParcel(Parcel source) { + int logicalAddress = source.readInt(); + int physicalAddress = source.readInt(); + int deviceType = source.readInt(); + int vendorId = source.readInt(); + String displayName = source.readString(); + return new HdmiCecDeviceInfo(logicalAddress, physicalAddress, deviceType, + vendorId, displayName); + } + + @Override + public HdmiCecDeviceInfo[] newArray(int size) { + return new HdmiCecDeviceInfo[size]; + } + }; + + /** + * Constructor. + * + * @param logicalAddress logical address of HDMI-Cec device. + * For more details, refer {@link HdmiCec} + * @param physicalAddress physical address of HDMI-Cec device + * @param deviceType type of device. For more details, refer {@link HdmiCec} + * @param vendorId vendor id of device. It's used for vendor specific command + * @param displayName name of device + * @hide + */ + public HdmiCecDeviceInfo(int logicalAddress, int physicalAddress, int deviceType, + int vendorId, String displayName) { + mLogicalAddress = logicalAddress; + mPhysicalAddress = physicalAddress; + mDeviceType = deviceType; + mDisplayName = displayName; + mVendorId = vendorId; + } + + /** + * Return the logical address of the device. It can have 0-15 values. + * For more details, refer constants between {@link HdmiCec#ADDR_TV} + * and {@link HdmiCec#ADDR_UNREGISTERED}. + */ + public int getLogicalAddress() { + return mLogicalAddress; + } + + /** + * Return the physical address of the device. + */ + public int getPhysicalAddress() { + return mPhysicalAddress; + } + + /** + * Return type of the device. For more details, refer constants between + * {@link HdmiCec#DEVICE_TV} and {@link HdmiCec#DEVICE_INACTIVE}. + */ + public int getDeviceType() { + return mDeviceType; + } + + /** + * Return display (OSD) name of the device. + */ + public String getDisplayName() { + return mDisplayName; + } + + /** + * Return vendor id of the device. Vendor id is used to distinguish devices + * built by other manufactures. This is required for vendor-specific command + * on CEC standard. + */ + public int getVendorId() { + return mVendorId; + } + + /** + * Describe the kinds of special objects contained in this Parcelable's + * marshalled representation. + */ + @Override + public int describeContents() { + return 0; + } + + /** + * Serialize this object into a {@link Parcel}. + * + * @param dest The Parcel in which the object should be written. + * @param flags Additional flags about how the object should be written. + * May be 0 or {@link Parcelable#PARCELABLE_WRITE_RETURN_VALUE}. + */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mLogicalAddress); + dest.writeInt(mPhysicalAddress); + dest.writeInt(mDeviceType); + dest.writeInt(mVendorId); + dest.writeString(mDisplayName); + } + + @Override + public String toString() { + StringBuffer s = new StringBuffer(); + s.append("logical_address: ").append(mLogicalAddress).append(", "); + s.append("physical_address: ").append(mPhysicalAddress).append(", "); + s.append("device_type: ").append(mDeviceType).append(", "); + s.append("vendor_id: ").append(mVendorId).append(", "); + s.append("display_name: ").append(mDisplayName); + return s.toString(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof HdmiCecDeviceInfo)) { + return false; + } + + HdmiCecDeviceInfo other = (HdmiCecDeviceInfo) obj; + return mLogicalAddress == other.mLogicalAddress + && mPhysicalAddress == other.mPhysicalAddress + && mDeviceType == other.mDeviceType + && mVendorId == other.mVendorId + && mDisplayName.equals(other.mDisplayName); + } +} 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/GeofenceHardware.java b/core/java/android/hardware/location/GeofenceHardware.java index 21de9f5..4c074e9 100644 --- a/core/java/android/hardware/location/GeofenceHardware.java +++ b/core/java/android/hardware/location/GeofenceHardware.java @@ -79,7 +79,7 @@ public final class GeofenceHardware { */ public static final int MONITOR_UNSUPPORTED = 2; - // The following constants need to match geofence flags in gps.h + // The following constants need to match geofence flags in gps.h and fused_location.h /** * The constant to indicate that the user has entered the geofence. */ @@ -92,7 +92,7 @@ public final class GeofenceHardware { /** * The constant to indicate that the user is uncertain with respect to a - * geofence. nn + * geofence. */ public static final int GEOFENCE_UNCERTAIN = 1<<2L; 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..e6dbcd0 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -249,6 +249,18 @@ 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; + + /** + * Passing this flag into a call to {@link #setCursorAnchorMonitorMode(int)} will result in + * the cursor rectangle being provided in screen coordinates to subsequent + * {@link #onUpdateCursor(Rect)} callbacks. + */ + public static final int CURSOR_ANCHOR_MONITOR_MODE_CURSOR_RECT = 0x1; + InputMethodManager mImm; int mTheme = 0; @@ -642,12 +654,11 @@ public class InputMethodService extends AbstractInputMethodService { return false; } - @Override public void onCreate() { - mTheme = Resources.selectSystemTheme(mTheme, - getApplicationInfo().targetSdkVersion, - android.R.style.Theme_InputMethod, - android.R.style.Theme_Holo_InputMethod, - android.R.style.Theme_DeviceDefault_InputMethod); + @Override + public void onCreate() { + mTheme = getResources().selectSystemTheme(mTheme, getApplicationInfo().targetSdkVersion, + com.android.internal.R.array.system_theme_sdks, + com.android.internal.R.array.system_theme_ime_styles); super.setTheme(mTheme); super.onCreate(); mImm = (InputMethodManager)getSystemService(INPUT_METHOD_SERVICE); @@ -1692,15 +1703,24 @@ public class InputMethodService extends AbstractInputMethodService { } /** - * Called when the application has reported a new location of its text - * cursor. This is only called if explicitly requested by the input method. - * The default implementation does nothing. + * Called when the application has reported a new location of its text cursor. This is only + * called if explicitly requested by the input method. The default implementation does nothing. + * @param newCursor The new cursor position, in screen coordinates if the input method calls + * {@link #setCursorAnchorMonitorMode} with {@link #CURSOR_ANCHOR_MONITOR_MODE_CURSOR_RECT}. + * Otherwise, this is in local coordinates. */ public void onUpdateCursor(Rect newCursor) { // Intentionally empty } /** + * 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 +2342,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/INetworkScoreCache.aidl b/core/java/android/net/INetworkScoreCache.aidl new file mode 100644 index 0000000..35601ce --- /dev/null +++ b/core/java/android/net/INetworkScoreCache.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.net; + +import android.net.ScoredNetwork; + +/** + * A service which stores a subset of scored networks from the active network scorer. + * + * <p>To be implemented by network subsystems (e.g. Wi-Fi). NetworkScoreService will propagate + * scores down to each subsystem depending on the network type. Implementations may register for + * a given network type by calling NetworkScoreManager.registerNetworkSubsystem. + * + * <p>A proper implementation should throw SecurityException whenever the caller is not privileged. + * It may request scores by calling NetworkScoreManager#requestScores(NetworkKey[]); a call to + * updateScores may follow but may not depending on the active scorer's implementation, and in + * general this method may be called at any time. + * + * <p>Implementations should also override dump() so that "adb shell dumpsys network_score" includes + * the current scores for each network for debugging purposes. + * @hide + */ +interface INetworkScoreCache +{ + void updateScores(in List<ScoredNetwork> networks); + + void clearScores(); +} + diff --git a/core/java/android/net/INetworkScoreService.aidl b/core/java/android/net/INetworkScoreService.aidl new file mode 100644 index 0000000..626bd2a --- /dev/null +++ b/core/java/android/net/INetworkScoreService.aidl @@ -0,0 +1,61 @@ +/** + * 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.net.INetworkScoreCache; +import android.net.ScoredNetwork; + +/** + * A service for updating network scores from a network scorer application. + * @hide + */ +interface INetworkScoreService +{ + /** + * Update scores. + * @return whether the update was successful. + * @throws SecurityException if the caller is not the current active scorer. + */ + boolean updateScores(in ScoredNetwork[] networks); + + /** + * Clear all scores. + * @return whether the clear was successful. + * @throws SecurityException if the caller is neither the current active scorer nor the system. + */ + boolean clearScores(); + + /** + * Set the active scorer and clear existing scores. + * @param packageName the package name of the new scorer to use. + * @return true if the operation succeeded, or false if the new package is not a valid scorer. + * @throws SecurityException if the caller is not the system. + */ + boolean setActiveScorer(in String packageName); + + /** + * Register a network subsystem for scoring. + * + * @param networkType the type of network this cache can handle. See {@link NetworkKey#type}. + * @param scoreCache implementation of {@link INetworkScoreCache} to store the scores. + * @throws SecurityException if the caller is not the system. + * @throws IllegalArgumentException if a score cache is already registed for this type. + * @hide + */ + void registerNetworkScoreCache(int networkType, INetworkScoreCache scoreCache); + +} 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..30b61c5 100644 --- a/core/java/android/net/MobileDataStateTracker.java +++ b/core/java/android/net/MobileDataStateTracker.java @@ -54,7 +54,7 @@ import java.util.concurrent.atomic.AtomicBoolean; public class MobileDataStateTracker extends BaseNetworkStateTracker { private static final String TAG = "MobileDataStateTracker"; - private static final boolean DBG = true; + private static final boolean DBG = false; private static final boolean VDBG = false; private PhoneConstants.DataState mMobileDataState; @@ -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..bc19658 --- /dev/null +++ b/core/java/android/net/NetworkKey.java @@ -0,0 +1,128 @@ +/* + * 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.Objects; + +/** + * Information which identifies a specific network. + * + * @hide + */ +// NOTE: Ideally, we would abstract away the details of what identifies a network of a specific +// type, so that all networks appear the same and can be scored without concern to the network type +// itself. However, because no such cross-type identifier currently exists in the Android framework, +// and because systems might obtain information about networks from sources other than Android +// devices, we need to provide identifying details about each specific network type (wifi, cell, +// etc.) so that clients can pull out these details depending on the type of network. +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 boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + NetworkKey that = (NetworkKey) o; + + return type == that.type && Objects.equals(wifiKey, that.wifiKey); + } + + @Override + public int hashCode() { + return Objects.hash(type, wifiKey); + } + + @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..352512e --- /dev/null +++ b/core/java/android/net/NetworkScoreManager.java @@ -0,0 +1,206 @@ +/* + * 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; +import android.content.Intent; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; + +/** + * 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 an active 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 active scorer with + * {@link #getActiveScorerPackage()} and request to change the active scorer by sending an + * {@link #ACTION_CHANGE_ACTIVE} broadcast with another scorer. + * + * @hide + */ +public class NetworkScoreManager { + /** + * Activity action: ask the user to change the active network scorer. This will show a dialog + * that asks the user whether they want to replace the current active scorer with the one + * specified in {@link #EXTRA_PACKAGE_NAME}. The activity will finish with RESULT_OK if the + * active scorer was changed or RESULT_CANCELED if it failed for any reason. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_CHANGE_ACTIVE = "android.net.scoring.CHANGE_ACTIVE"; + + /** + * Extra used with {@link #ACTION_CHANGE_ACTIVE} 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 active 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; + private final INetworkScoreService mService; + + /** @hide */ + public NetworkScoreManager(Context context) { + mContext = context; + IBinder iBinder = ServiceManager.getService(Context.NETWORK_SCORE_SERVICE); + mService = INetworkScoreService.Stub.asInterface(iBinder); + } + + /** + * Obtain the package name of the current active network scorer. + * + * <p>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_ACTIVE} intent. + * @return the full package name of the current active scorer, or null if there is no active + * scorer. + */ + public String getActiveScorerPackage() { + return NetworkScorerAppManager.getActiveScorer(mContext); + } + + /** + * Update network scores. + * + * <p>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. + * @return whether the update was successful. + * @throws SecurityException if the caller is not the active scorer. + */ + public boolean updateScores(ScoredNetwork[] networks) throws SecurityException { + try { + return mService.updateScores(networks); + } catch (RemoteException e) { + return false; + } + } + + /** + * Clear network scores. + * + * <p>Should be called when all scores need to be invalidated, i.e. because the scoring + * algorithm has changed and old scores can no longer be compared to future scores. + * + * <p>Note that scores will be cleared automatically when the active scorer changes, as scores + * from one scorer cannot be compared to those from another scorer. + * + * @return whether the clear was successful. + * @throws SecurityException if the caller is not the active scorer or privileged. + */ + public boolean clearScores() throws SecurityException { + try { + return mService.clearScores(); + } catch (RemoteException e) { + return false; + } + } + + /** + * Set the active scorer to a new package and clear existing scores. + * + * @return true if the operation succeeded, or false if the new package is not a valid scorer. + * @throws SecurityException if the caller does not hold the + * {@link android.Manifest.permission#BROADCAST_SCORE_NETWORKS} permission indicating + * that it can manage scorer applications. + * @hide + */ + public boolean setActiveScorer(String packageName) throws SecurityException { + try { + return mService.setActiveScorer(packageName); + } catch (RemoteException e) { + return false; + } + } + + /** + * Request scoring for networks. + * + * <p>Note that this is just a helper method to assemble the broadcast, and will run in the + * calling process. + * + * @return true if the broadcast was sent, or false if there is no active scorer. + * @throws SecurityException if the caller does not hold the + * {@link android.Manifest.permission#BROADCAST_SCORE_NETWORKS} permission. + * @hide + */ + public boolean requestScores(NetworkKey[] networks) throws SecurityException { + String activeScorer = getActiveScorerPackage(); + if (activeScorer == null) { + return false; + } + Intent intent = new Intent(ACTION_SCORE_NETWORKS); + intent.setPackage(activeScorer); + intent.putExtra(EXTRA_NETWORKS_TO_SCORE, networks); + mContext.sendBroadcast(intent); + return true; + } + + /** + * Register a network score cache. + * + * @param networkType the type of network this cache can handle. See {@link NetworkKey#type}. + * @param scoreCache implementation of {@link INetworkScoreCache} to store the scores. + * @throws SecurityException if the caller does not hold the + * {@link android.Manifest.permission#BROADCAST_SCORE_NETWORKS} permission. + * @throws IllegalArgumentException if a score cache is already registered for this type. + * @hide + */ + public void registerNetworkScoreCache(int networkType, INetworkScoreCache scoreCache) { + try { + mService.registerNetworkScoreCache(networkType, scoreCache); + } catch (RemoteException e) { + } + } +} diff --git a/core/java/android/net/NetworkScorerAppManager.java b/core/java/android/net/NetworkScorerAppManager.java new file mode 100644 index 0000000..3660e7a --- /dev/null +++ b/core/java/android/net/NetworkScorerAppManager.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.net; + +import android.Manifest.permission; +import android.app.AppOpsManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.provider.Settings; +import android.provider.Settings.Global; +import android.text.TextUtils; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * Internal class for managing the primary network scorer application. + * + * @hide + */ +public final class NetworkScorerAppManager { + private static final String TAG = "NetworkScorerAppManager"; + + private static final Intent SCORE_INTENT = + new Intent(NetworkScoreManager.ACTION_SCORE_NETWORKS); + + /** This class cannot be instantiated. */ + private NetworkScorerAppManager() {} + + /** + * Returns the list of available scorer app package names. + * + * <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 NetworkScoreManager#ACTION_SCORE_NETWORKS} guarded by the + * {@link android.Manifest.permission#BROADCAST_SCORE_NETWORKS} permission. + * </ul> + * + * @return the list of scorers, or the empty list if there are no valid scorers. + */ + public static Collection<String> getAllValidScorers(Context context) { + List<String> scorers = new ArrayList<>(); + + PackageManager pm = context.getPackageManager(); + List<ResolveInfo> receivers = pm.queryBroadcastReceivers(SCORE_INTENT, 0 /* flags */); + for (ResolveInfo receiver : receivers) { + // This field is a misnomer, see android.content.pm.ResolveInfo#activityInfo + final ActivityInfo receiverInfo = receiver.activityInfo; + if (receiverInfo == null) { + // Should never happen with queryBroadcastReceivers, but invalid nonetheless. + continue; + } + if (!permission.BROADCAST_SCORE_NETWORKS.equals(receiverInfo.permission)) { + // Receiver doesn't require the BROADCAST_SCORE_NETWORKS permission, which means + // anyone could trigger network scoring and flood the framework with score requests. + continue; + } + if (pm.checkPermission(permission.SCORE_NETWORKS, receiverInfo.packageName) != + PackageManager.PERMISSION_GRANTED) { + // Application doesn't hold the SCORE_NETWORKS permission, so the user never + // approved it as a network scorer. + continue; + } + scorers.add(receiverInfo.packageName); + } + + return scorers; + } + + /** + * Get the application package name to use for scoring networks. + * + * @return the scorer package or null if scoring is disabled (including if no scorer was ever + * selected) or if the previously-set scorer is no longer a valid scorer app (e.g. because + * it was disabled or uninstalled). + */ + public static String getActiveScorer(Context context) { + String scorerPackage = Settings.Global.getString(context.getContentResolver(), + Global.NETWORK_SCORER_APP); + if (isPackageValidScorer(context, scorerPackage)) { + return scorerPackage; + } else { + return null; + } + } + + /** + * Set the specified package as the default scorer application. + * + * <p>The caller must have permission to write to {@link Settings.Global}. + * + * @param context the context of the calling application + * @param packageName the packageName of the new scorer to use. If null, scoring will be + * disabled. Otherwise, the scorer will only be set if it is a valid scorer application. + * @return true if the scorer was changed, or false if the package is not a valid scorer. + */ + public static boolean setActiveScorer(Context context, String packageName) { + String oldPackageName = Settings.Global.getString(context.getContentResolver(), + Settings.Global.NETWORK_SCORER_APP); + if (TextUtils.equals(oldPackageName, packageName)) { + // No change. + return true; + } + + Log.i(TAG, "Changing network scorer from " + oldPackageName + " to " + packageName); + + if (packageName == null) { + Settings.Global.putString(context.getContentResolver(), Global.NETWORK_SCORER_APP, + null); + return true; + } else { + // We only make the change if the new package is valid. + if (isPackageValidScorer(context, packageName)) { + Settings.Global.putString(context.getContentResolver(), + Settings.Global.NETWORK_SCORER_APP, packageName); + return true; + } else { + Log.w(TAG, "Requested network scorer is not valid: " + packageName); + return false; + } + } + } + + /** Determine whether the application with the given UID is the enabled scorer. */ + public static boolean isCallerActiveScorer(Context context, int callingUid) { + String defaultApp = getActiveScorer(context); + if (defaultApp == null) { + return false; + } + AppOpsManager appOpsMgr = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); + try { + appOpsMgr.checkPackage(callingUid, defaultApp); + return true; + } catch (SecurityException e) { + return false; + } + } + + /** Returns true if the given package is a valid scorer. */ + public static boolean isPackageValidScorer(Context context, String packageName) { + Collection<String> applications = getAllValidScorers(context); + return packageName != null && applications.contains(packageName); + } +} 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..bea8d1c 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,78 +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(); - } - } - } - - static class AndroidProxySelectorRoutePlanner - extends org.apache.http.impl.conn.ProxySelectorRoutePlanner { - - private Context mContext; - - public AndroidProxySelectorRoutePlanner(SchemeRegistry schreg, ProxySelector prosel, - Context context) { - super(schreg, prosel); - mContext = context; - } - - @Override - protected java.net.Proxy chooseProxy(List<java.net.Proxy> proxies, HttpHost target, - HttpRequest request, HttpContext context) { - return getProxy(mContext, target.getHostName()); - } - - @Override - protected HttpHost determineProxy(HttpHost target, HttpRequest request, - HttpContext context) { - return getPreferredHttpHost(mContext, target.getHostName()); - } - - @Override - public HttpRoute determineRoute(HttpHost target, HttpRequest request, - HttpContext context) { - HttpHost proxy = getPreferredHttpHost(mContext, target.getHostName()); - if (proxy == null) { - return new HttpRoute(target); - } else { - return new HttpRoute(target, null, proxy, false); + return PROXY_PORT_INVALID; } + if (portVal <= 0 || portVal > 0xFFFF) return PROXY_PORT_INVALID; } - } - - /** @hide */ - public static final HttpRoutePlanner getAndroidProxySelectorRoutePlanner(Context context) { - AndroidProxySelectorRoutePlanner ret = new AndroidProxySelectorRoutePlanner( - new SchemeRegistry(), ProxySelector.getDefault(), context); - return ret; + return PROXY_VALID; } /** @hide */ 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..dd744d3 --- /dev/null +++ b/core/java/android/net/RssiCurve.java @@ -0,0 +1,177 @@ +/* + * 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.Arrays; +import java.util.Objects; + +/** + * 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); + } + + /** + * Lookup the score for a given RSSI value. + * + * @param rssi The RSSI to lookup. If the RSSI falls below the start of the curve, the score at + * the start of the curve will be returned. If it falls after the end of the curve, the + * score at the end of the curve will be returned. + * @return the score for the given RSSI. + */ + public byte lookupScore(int rssi) { + int index = (rssi - start) / bucketWidth; + + // Snap the index to the closest bucket if it falls outside the curve. + if (index < 0) { + index = 0; + } else if (index > rssiBuckets.length - 1) { + index = rssiBuckets.length - 1; + } + + return rssiBuckets[index]; + } + + /** + * Determine if two RSSI curves are defined in the same way. + * + * <p>Note that two curves can be equivalent but defined differently, e.g. if one bucket in one + * curve is split into two buckets in another. For the purpose of this method, these curves are + * not considered equal to each other. + */ + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + RssiCurve rssiCurve = (RssiCurve) o; + + return start == rssiCurve.start && + bucketWidth == rssiCurve.bucketWidth && + Arrays.equals(rssiBuckets, rssiCurve.rssiBuckets); + } + + @Override + public int hashCode() { + return Objects.hash(start, bucketWidth, 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..7902313 --- /dev/null +++ b/core/java/android/net/ScoredNetwork.java @@ -0,0 +1,117 @@ +/* + * 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.Objects; + +/** + * 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 boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ScoredNetwork that = (ScoredNetwork) o; + + return Objects.equals(networkKey, that.networkKey) && + Objects.equals(rssiCurve, that.rssiCurve); + } + + @Override + public int hashCode() { + return Objects.hash(networkKey, rssiCurve); + } + + @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/Uri.java b/core/java/android/net/Uri.java index a7a8a0a..ce70455 100644 --- a/core/java/android/net/Uri.java +++ b/core/java/android/net/Uri.java @@ -21,6 +21,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.StrictMode; import android.util.Log; + import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodingException; @@ -32,8 +33,10 @@ import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; +import java.util.Objects; import java.util.RandomAccess; import java.util.Set; + import libcore.net.UriCodec; /** @@ -2338,4 +2341,29 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { StrictMode.onFileUriExposed(location); } } + + /** + * Test if this is a path prefix match against the given Uri. Verifies that + * scheme, authority, and atomic path segments match. + * + * @hide + */ + public boolean isPathPrefixMatch(Uri prefix) { + if (!Objects.equals(getScheme(), prefix.getScheme())) return false; + if (!Objects.equals(getAuthority(), prefix.getAuthority())) return false; + + List<String> seg = getPathSegments(); + List<String> prefixSeg = prefix.getPathSegments(); + + final int prefixSize = prefixSeg.size(); + if (seg.size() < prefixSize) return false; + + for (int i = 0; i < prefixSize; i++) { + if (!Objects.equals(seg.get(i), prefixSeg.get(i))) { + return false; + } + } + + return true; + } } diff --git a/core/java/android/net/WifiKey.java b/core/java/android/net/WifiKey.java new file mode 100644 index 0000000..9e92e89 --- /dev/null +++ b/core/java/android/net/WifiKey.java @@ -0,0 +1,122 @@ +/* + * 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.Objects; +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 boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + WifiKey wifiKey = (WifiKey) o; + + return Objects.equals(ssid, wifiKey.ssid) && Objects.equals(bssid, wifiKey.bssid); + } + + @Override + public int hashCode() { + return Objects.hash(ssid, 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/INfcCardEmulation.aidl b/core/java/android/nfc/INfcCardEmulation.aidl index b8a5ba7..ae9796b 100644 --- a/core/java/android/nfc/INfcCardEmulation.aidl +++ b/core/java/android/nfc/INfcCardEmulation.aidl @@ -17,6 +17,7 @@ package android.nfc; import android.content.ComponentName; +import android.nfc.cardemulation.AidGroup; import android.nfc.cardemulation.ApduServiceInfo; import android.os.RemoteCallback; @@ -29,5 +30,8 @@ interface INfcCardEmulation boolean isDefaultServiceForAid(int userHandle, in ComponentName service, String aid); boolean setDefaultServiceForCategory(int userHandle, in ComponentName service, String category); boolean setDefaultForNextTap(int userHandle, in ComponentName service); + boolean registerAidGroupForService(int userHandle, in ComponentName service, in AidGroup aidGroup); + AidGroup getAidGroupForService(int userHandle, in ComponentName service, String category); + boolean removeAidGroupForService(int userHandle, in ComponentName service, String category); List<ApduServiceInfo> getServices(int userHandle, in String category); } 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/cardemulation/AidGroup.aidl b/core/java/android/nfc/cardemulation/AidGroup.aidl new file mode 100644 index 0000000..56d6fa5 --- /dev/null +++ b/core/java/android/nfc/cardemulation/AidGroup.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc.cardemulation; + +parcelable AidGroup; diff --git a/core/java/android/nfc/cardemulation/AidGroup.java b/core/java/android/nfc/cardemulation/AidGroup.java new file mode 100644 index 0000000..2820f40 --- /dev/null +++ b/core/java/android/nfc/cardemulation/AidGroup.java @@ -0,0 +1,165 @@ +package android.nfc.cardemulation; + +import java.io.IOException; +import java.util.ArrayList; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + +/** + * The AidGroup class represents a group of ISO/IEC 7816-4 + * Application Identifiers (AIDs) for a specific application + * category, along with a description resource describing + * the group. + */ +public final class AidGroup implements Parcelable { + /** + * The maximum number of AIDs that can be present in any one group. + */ + public static final int MAX_NUM_AIDS = 256; + + static final String TAG = "AidGroup"; + + final ArrayList<String> aids; + final String category; + final String description; + + /** + * Creates a new AidGroup object. + * + * @param aids The list of AIDs present in the group + * @param category The category of this group + */ + public AidGroup(ArrayList<String> aids, String category) { + if (aids == null || aids.size() == 0) { + throw new IllegalArgumentException("No AIDS in AID group."); + } + if (aids.size() > MAX_NUM_AIDS) { + throw new IllegalArgumentException("Too many AIDs in AID group."); + } + if (!isValidCategory(category)) { + throw new IllegalArgumentException("Category specified is not valid."); + } + this.aids = aids; + this.category = category; + this.description = null; + } + + AidGroup(String category, String description) { + this.aids = new ArrayList<String>(); + this.category = category; + this.description = description; + } + + /** + * @return the category of this AID group + */ + public String getCategory() { + return category; + } + + /** + * @return the list of AIDs in this group + */ + public ArrayList<String> getAids() { + return aids; + } + + @Override + public String toString() { + StringBuilder out = new StringBuilder("Category: " + category + + ", AIDs:"); + for (String aid : aids) { + out.append(aid); + out.append(", "); + } + return out.toString(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(category); + dest.writeInt(aids.size()); + if (aids.size() > 0) { + dest.writeStringList(aids); + } + } + + public static final Parcelable.Creator<AidGroup> CREATOR = + new Parcelable.Creator<AidGroup>() { + + @Override + public AidGroup createFromParcel(Parcel source) { + String category = source.readString(); + int listSize = source.readInt(); + ArrayList<String> aidList = new ArrayList<String>(); + if (listSize > 0) { + source.readStringList(aidList); + } + return new AidGroup(aidList, category); + } + + @Override + public AidGroup[] newArray(int size) { + return new AidGroup[size]; + } + }; + + /** + * @hide + * Note: description is not serialized, since it's not localized + * and resource identifiers don't make sense to persist. + */ + static public AidGroup createFromXml(XmlPullParser parser) throws XmlPullParserException, IOException { + String category = parser.getAttributeValue(null, "category"); + ArrayList<String> aids = new ArrayList<String>(); + int eventType = parser.getEventType(); + int minDepth = parser.getDepth(); + while (eventType != XmlPullParser.END_DOCUMENT && parser.getDepth() >= minDepth) { + if (eventType == XmlPullParser.START_TAG) { + String tagName = parser.getName(); + if (tagName.equals("aid")) { + String aid = parser.getAttributeValue(null, "value"); + if (aid != null) { + aids.add(aid); + } + } else { + Log.d(TAG, "Ignorning unexpected tag: " + tagName); + } + } + eventType = parser.next(); + } + if (category != null && aids.size() > 0) { + return new AidGroup(aids, category); + } else { + return null; + } + } + + /** + * @hide + */ + public void writeAsXml(XmlSerializer out) throws IOException { + out.attribute(null, "category", category); + for (String aid : aids) { + out.startTag(null, "aid"); + out.attribute(null, "value", aid); + out.endTag(null, "aid"); + } + } + + boolean isValidCategory(String category) { + return CardEmulation.CATEGORY_PAYMENT.equals(category) || + CardEmulation.CATEGORY_OTHER.equals(category); + } +} diff --git a/core/java/android/nfc/cardemulation/ApduServiceInfo.java b/core/java/android/nfc/cardemulation/ApduServiceInfo.java index d7ef4bc..94f35ed 100644 --- a/core/java/android/nfc/cardemulation/ApduServiceInfo.java +++ b/core/java/android/nfc/cardemulation/ApduServiceInfo.java @@ -35,9 +35,12 @@ import android.util.Xml; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; +import java.io.FileDescriptor; import java.io.IOException; +import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashMap; +import java.util.Map; /** * @hide @@ -56,24 +59,19 @@ public final class ApduServiceInfo implements Parcelable { final String mDescription; /** - * Convenience AID list - */ - final ArrayList<String> mAids; - - /** * Whether this service represents AIDs running on the host CPU */ final boolean mOnHost; /** - * All AID groups this service handles + * Mapping from category to static AID group */ - final ArrayList<AidGroup> mAidGroups; + final HashMap<String, AidGroup> mStaticAidGroups; /** - * Convenience hashmap + * Mapping from category to dynamic AID group */ - final HashMap<String, AidGroup> mCategoryToGroup; + final HashMap<String, AidGroup> mDynamicAidGroups; /** * Whether this service should only be started when the device is unlocked. @@ -86,26 +84,33 @@ public final class ApduServiceInfo implements Parcelable { final int mBannerResourceId; /** + * The uid of the package the service belongs to + */ + final int mUid; + /** * @hide */ public ApduServiceInfo(ResolveInfo info, boolean onHost, String description, - ArrayList<AidGroup> aidGroups, boolean requiresUnlock, int bannerResource) { + ArrayList<AidGroup> staticAidGroups, ArrayList<AidGroup> dynamicAidGroups, + boolean requiresUnlock, int bannerResource, int uid) { this.mService = info; this.mDescription = description; - this.mAidGroups = aidGroups; - this.mAids = new ArrayList<String>(); - this.mCategoryToGroup = new HashMap<String, AidGroup>(); + this.mStaticAidGroups = new HashMap<String, AidGroup>(); + this.mDynamicAidGroups = new HashMap<String, AidGroup>(); this.mOnHost = onHost; this.mRequiresDeviceUnlock = requiresUnlock; - for (AidGroup aidGroup : aidGroups) { - this.mCategoryToGroup.put(aidGroup.category, aidGroup); - this.mAids.addAll(aidGroup.aids); + for (AidGroup aidGroup : staticAidGroups) { + this.mStaticAidGroups.put(aidGroup.category, aidGroup); + } + for (AidGroup aidGroup : dynamicAidGroups) { + this.mDynamicAidGroups.put(aidGroup.category, aidGroup); } this.mBannerResourceId = bannerResource; + this.mUid = uid; } - public ApduServiceInfo(PackageManager pm, ResolveInfo info, boolean onHost) - throws XmlPullParserException, IOException { + public ApduServiceInfo(PackageManager pm, ResolveInfo info, boolean onHost) throws + XmlPullParserException, IOException { ServiceInfo si = info.serviceInfo; XmlResourceParser parser = null; try { @@ -163,10 +168,10 @@ public final class ApduServiceInfo implements Parcelable { sa.recycle(); } - mAidGroups = new ArrayList<AidGroup>(); - mCategoryToGroup = new HashMap<String, AidGroup>(); - mAids = new ArrayList<String>(); + mStaticAidGroups = new HashMap<String, AidGroup>(); + mDynamicAidGroups = new HashMap<String, AidGroup>(); mOnHost = onHost; + final int depth = parser.getDepth(); AidGroup currentGroup = null; @@ -179,14 +184,14 @@ public final class ApduServiceInfo implements Parcelable { final TypedArray groupAttrs = res.obtainAttributes(attrs, com.android.internal.R.styleable.AidGroup); // Get category of AID group - String groupDescription = groupAttrs.getString( - com.android.internal.R.styleable.AidGroup_description); String groupCategory = groupAttrs.getString( com.android.internal.R.styleable.AidGroup_category); + String groupDescription = groupAttrs.getString( + com.android.internal.R.styleable.AidGroup_description); if (!CardEmulation.CATEGORY_PAYMENT.equals(groupCategory)) { groupCategory = CardEmulation.CATEGORY_OTHER; } - currentGroup = mCategoryToGroup.get(groupCategory); + currentGroup = mStaticAidGroups.get(groupCategory); if (currentGroup != null) { if (!CardEmulation.CATEGORY_OTHER.equals(groupCategory)) { Log.e(TAG, "Not allowing multiple aid-groups in the " + @@ -200,9 +205,8 @@ public final class ApduServiceInfo implements Parcelable { } else if (eventType == XmlPullParser.END_TAG && "aid-group".equals(tagName) && currentGroup != null) { if (currentGroup.aids.size() > 0) { - if (!mCategoryToGroup.containsKey(currentGroup.category)) { - mAidGroups.add(currentGroup); - mCategoryToGroup.put(currentGroup.category, currentGroup); + if (!mStaticAidGroups.containsKey(currentGroup.category)) { + mStaticAidGroups.put(currentGroup.category, currentGroup); } } else { Log.e(TAG, "Not adding <aid-group> with empty or invalid AIDs"); @@ -216,7 +220,6 @@ public final class ApduServiceInfo implements Parcelable { toUpperCase(); if (isValidAid(aid) && !currentGroup.aids.contains(aid)) { currentGroup.aids.add(aid); - mAids.add(aid); } else { Log.e(TAG, "Ignoring invalid or duplicate aid: " + aid); } @@ -228,6 +231,8 @@ public final class ApduServiceInfo implements Parcelable { } finally { if (parser != null) parser.close(); } + // Set uid + mUid = si.applicationInfo.uid; } public ComponentName getComponent() { @@ -235,16 +240,58 @@ public final class ApduServiceInfo implements Parcelable { mService.serviceInfo.name); } + /** + * Returns a consolidated list of AIDs from the AID groups + * registered by this service. Note that if a service has both + * a static (manifest-based) AID group for a category and a dynamic + * AID group, only the dynamically registered AIDs will be returned + * for that category. + * @return List of AIDs registered by the service + */ public ArrayList<String> getAids() { - return mAids; + final ArrayList<String> aids = new ArrayList<String>(); + for (AidGroup group : getAidGroups()) { + aids.addAll(group.aids); + } + return aids; } + /** + * Returns the registered AID group for this category. + */ + public AidGroup getDynamicAidGroupForCategory(String category) { + return mDynamicAidGroups.get(category); + } + + public boolean removeDynamicAidGroupForCategory(String category) { + return (mDynamicAidGroups.remove(category) != null); + } + + /** + * Returns a consolidated list of AID groups + * registered by this service. Note that if a service has both + * a static (manifest-based) AID group for a category and a dynamic + * AID group, only the dynamically registered AID group will be returned + * for that category. + * @return List of AIDs registered by the service + */ public ArrayList<AidGroup> getAidGroups() { - return mAidGroups; + final ArrayList<AidGroup> groups = new ArrayList<AidGroup>(); + for (Map.Entry<String, AidGroup> entry : mDynamicAidGroups.entrySet()) { + groups.add(entry.getValue()); + } + for (Map.Entry<String, AidGroup> entry : mStaticAidGroups.entrySet()) { + if (!mDynamicAidGroups.containsKey(entry.getKey())) { + // Consolidate AID groups - don't return static ones + // if a dynamic group exists for the category. + groups.add(entry.getValue()); + } + } + return groups; } public boolean hasCategory(String category) { - return mCategoryToGroup.containsKey(category); + return (mStaticAidGroups.containsKey(category) || mDynamicAidGroups.containsKey(category)); } public boolean isOnHost() { @@ -259,6 +306,14 @@ public final class ApduServiceInfo implements Parcelable { return mDescription; } + public int getUid() { + return mUid; + } + + public void setOrReplaceDynamicAidGroup(AidGroup aidGroup) { + mDynamicAidGroups.put(aidGroup.getCategory(), aidGroup); + } + public CharSequence loadLabel(PackageManager pm) { return mService.loadLabel(pm); } @@ -304,8 +359,12 @@ public final class ApduServiceInfo implements Parcelable { StringBuilder out = new StringBuilder("ApduService: "); out.append(getComponent()); out.append(", description: " + mDescription); - out.append(", AID Groups: "); - for (AidGroup aidGroup : mAidGroups) { + out.append(", Static AID Groups: "); + for (AidGroup aidGroup : mStaticAidGroups.values()) { + out.append(aidGroup.toString()); + } + out.append(", Dynamic AID Groups: "); + for (AidGroup aidGroup : mDynamicAidGroups.values()) { out.append(aidGroup.toString()); } return out.toString(); @@ -336,12 +395,17 @@ public final class ApduServiceInfo implements Parcelable { mService.writeToParcel(dest, flags); dest.writeString(mDescription); dest.writeInt(mOnHost ? 1 : 0); - dest.writeInt(mAidGroups.size()); - if (mAidGroups.size() > 0) { - dest.writeTypedList(mAidGroups); + dest.writeInt(mStaticAidGroups.size()); + if (mStaticAidGroups.size() > 0) { + dest.writeTypedList(new ArrayList<AidGroup>(mStaticAidGroups.values())); + } + dest.writeInt(mDynamicAidGroups.size()); + if (mDynamicAidGroups.size() > 0) { + dest.writeTypedList(new ArrayList<AidGroup>(mDynamicAidGroups.values())); } dest.writeInt(mRequiresDeviceUnlock ? 1 : 0); dest.writeInt(mBannerResourceId); + dest.writeInt(mUid); }; public static final Parcelable.Creator<ApduServiceInfo> CREATOR = @@ -351,14 +415,21 @@ public final class ApduServiceInfo implements Parcelable { ResolveInfo info = ResolveInfo.CREATOR.createFromParcel(source); String description = source.readString(); boolean onHost = (source.readInt() != 0) ? true : false; - ArrayList<AidGroup> aidGroups = new ArrayList<AidGroup>(); - int numGroups = source.readInt(); - if (numGroups > 0) { - source.readTypedList(aidGroups, AidGroup.CREATOR); + ArrayList<AidGroup> staticAidGroups = new ArrayList<AidGroup>(); + int numStaticGroups = source.readInt(); + if (numStaticGroups > 0) { + source.readTypedList(staticAidGroups, AidGroup.CREATOR); + } + ArrayList<AidGroup> dynamicAidGroups = new ArrayList<AidGroup>(); + int numDynamicGroups = source.readInt(); + if (numDynamicGroups > 0) { + source.readTypedList(dynamicAidGroups, AidGroup.CREATOR); } boolean requiresUnlock = (source.readInt() != 0) ? true : false; int bannerResource = source.readInt(); - return new ApduServiceInfo(info, onHost, description, aidGroups, requiresUnlock, bannerResource); + int uid = source.readInt(); + return new ApduServiceInfo(info, onHost, description, staticAidGroups, + dynamicAidGroups, requiresUnlock, bannerResource, uid); } @Override @@ -367,76 +438,22 @@ public final class ApduServiceInfo implements Parcelable { } }; - public static class AidGroup implements Parcelable { - final ArrayList<String> aids; - final String category; - final String description; - - AidGroup(ArrayList<String> aids, String category, String description) { - this.aids = aids; - this.category = category; - this.description = description; - } - - AidGroup(String category, String description) { - this.aids = new ArrayList<String>(); - this.category = category; - this.description = description; - } - - public String getCategory() { - return category; - } - - public ArrayList<String> getAids() { - return aids; - } - - @Override - public String toString() { - StringBuilder out = new StringBuilder("Category: " + category + - ", description: " + description + ", AIDs:"); - for (String aid : aids) { - out.append(aid); - out.append(", "); + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println(" " + getComponent() + + " (Description: " + getDescription() + ")"); + pw.println(" Static AID groups:"); + for (AidGroup group : mStaticAidGroups.values()) { + pw.println(" Category: " + group.category); + for (String aid : group.aids) { + pw.println(" AID: " + aid); } - return out.toString(); } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(category); - dest.writeString(description); - dest.writeInt(aids.size()); - if (aids.size() > 0) { - dest.writeStringList(aids); + pw.println(" Dynamic AID groups:"); + for (AidGroup group : mDynamicAidGroups.values()) { + pw.println(" Category: " + group.category); + for (String aid : group.aids) { + pw.println(" AID: " + aid); } } - - public static final Parcelable.Creator<ApduServiceInfo.AidGroup> CREATOR = - new Parcelable.Creator<ApduServiceInfo.AidGroup>() { - - @Override - public AidGroup createFromParcel(Parcel source) { - String category = source.readString(); - String description = source.readString(); - int listSize = source.readInt(); - ArrayList<String> aidList = new ArrayList<String>(); - if (listSize > 0) { - source.readStringList(aidList); - } - return new AidGroup(aidList, category, description); - } - - @Override - public AidGroup[] newArray(int size) { - return new AidGroup[size]; - } - }; } } diff --git a/core/java/android/nfc/cardemulation/CardEmulation.java b/core/java/android/nfc/cardemulation/CardEmulation.java index 58d9616..41f039c 100644 --- a/core/java/android/nfc/cardemulation/CardEmulation.java +++ b/core/java/android/nfc/cardemulation/CardEmulation.java @@ -168,6 +168,10 @@ public final class CardEmulation { if (manager == null) { // Get card emu service INfcCardEmulation service = adapter.getCardEmulationService(); + if (service == null) { + Log.e(TAG, "This device does not implement the INfcCardEmulation interface."); + throw new UnsupportedOperationException(); + } manager = new CardEmulation(context, service); sCardEmus.put(context, manager); } @@ -271,6 +275,109 @@ public final class CardEmulation { } /** + * Registers a group of AIDs for the specified service. + * + * <p>If an AID group for that category was previously + * registered for this service (either statically + * through the manifest, or dynamically by using this API), + * that AID group will be replaced with this one. + * + * <p>Note that you can only register AIDs for a service that + * is running under the same UID as you are. Typically + * this means you need to call this from the same + * package as the service itself, though UIDs can also + * be shared between packages using shared UIDs. + * + * @param service The component name of the service + * @param aidGroup The group of AIDs to be registered + * @return whether the registration was successful. + */ + public boolean registerAidGroupForService(ComponentName service, AidGroup aidGroup) { + try { + return sService.registerAidGroupForService(UserHandle.myUserId(), service, aidGroup); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.registerAidGroupForService(UserHandle.myUserId(), service, + aidGroup); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return false; + } + } + } + + /** + * Retrieves the currently registered AID group for the specified + * category for a service. + * + * <p>Note that this will only return AID groups that were dynamically + * registered using {@link #registerAidGroupForService(ComponentName, AidGroup)} + * method. It will *not* return AID groups that were statically registered + * in the manifest. + * + * @param service The component name of the service + * @param category The category of the AID group to be returned, e.g. {@link #CATEGORY_PAYMENT} + * @return The AID group, or null if it couldn't be found + */ + public AidGroup getAidGroupForService(ComponentName service, String category) { + try { + return sService.getAidGroupForService(UserHandle.myUserId(), service, category); + } catch (RemoteException e) { + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return null; + } + try { + return sService.getAidGroupForService(UserHandle.myUserId(), service, category); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return null; + } + } + } + + /** + * Removes a registered AID group for the specified category for the + * service provided. + * + * <p>Note that this will only remove AID groups that were dynamically + * registered using the {@link #registerAidGroupForService(ComponentName, AidGroup)} + * method. It will *not* remove AID groups that were statically registered in + * the manifest. If a dynamically registered AID group is removed using + * this method, and a statically registered AID group for the same category + * exists in the manifest, that AID group will become active again. + * + * @param service The component name of the service + * @param category The category of the AID group to be removed, e.g. {@link #CATEGORY_PAYMENT} + * @return whether the group was successfully removed. + */ + public boolean removeAidGroupForService(ComponentName service, String category) { + try { + return sService.removeAidGroupForService(UserHandle.myUserId(), service, category); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.removeAidGroupForService(UserHandle.myUserId(), service, category); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return false; + } + } + } + + /** * @hide */ public boolean setDefaultServiceForCategory(ComponentName service, String category) { 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..9e9820f 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,25 +118,20 @@ 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", // and "unplugged". They were shortened for efficiency sake. - private static final String[] STAT_NAMES = { "t", "l", "c", "u" }; - + private static final String[] STAT_NAMES = { "l", "c", "u" }; + /** * Bump the version on this if the checkin format changes. */ @@ -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,14 @@ 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 static final String DISCHARGE_STEP_DATA = "dsd"; + private static final String CHARGE_STEP_DATA = "csd"; private final StringBuilder mFormatBuilder = new StringBuilder(32); private final Formatter mFormatter = new Formatter(mFormatBuilder); @@ -180,7 +188,7 @@ public abstract class BatteryStats implements Parcelable { * Returns the count associated with this Counter for the * selected type of statistics. * - * @param which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT */ public abstract int getCountLocked(int which); @@ -191,6 +199,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_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, 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 { @@ -199,7 +226,7 @@ public abstract class BatteryStats implements Parcelable { * Returns the count associated with this Timer for the * selected type of statistics. * - * @param which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT */ public abstract int getCountLocked(int which); @@ -207,11 +234,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 which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT + * @param elapsedRealtimeUs current elapsed realtime of system in microseconds + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, 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 +296,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 +340,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 +360,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,29 +380,34 @@ 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. + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. */ public abstract long getUserTime(int which); /** * Returns the total time (in 1/100 sec) spent executing in system code. * - * @param which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT. + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. */ public abstract long getSystemTime(int which); /** * Returns the number of times the process has been started. * - * @param which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT. + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. */ public abstract int getStarts(int which); /** * Returns the cpu time spent in microseconds while the process was in the foreground. - * @param which one of STATS_TOTAL, STATS_LAST, STATS_CURRENT or STATS_UNPLUGGED + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. * @return foreground cpu time in microseconds */ public abstract long getForegroundTime(int which); @@ -381,7 +416,7 @@ public abstract class BatteryStats implements Parcelable { * Returns the approximate cpu time spent in microseconds, at a certain CPU speed. * @param speedStep the index of the CPU speed. This is not the actual speed of the * CPU. - * @param which one of STATS_TOTAL, STATS_LAST, STATS_CURRENT or STATS_UNPLUGGED + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. * @see BatteryStats#getCpuSpeedSteps() */ public abstract long getTimeAtCpuSpeedStep(int speedStep, int which); @@ -400,7 +435,7 @@ public abstract class BatteryStats implements Parcelable { * Returns the number of times this package has done something that could wake up the * device from sleep. * - * @param which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT. + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. */ public abstract int getWakeups(int which); @@ -418,7 +453,7 @@ public abstract class BatteryStats implements Parcelable { * Returns the amount of time spent started. * * @param batteryUptime elapsed uptime on battery in microseconds. - * @param which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT. + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. * @return */ public abstract long getStartTime(long batteryUptime, int which); @@ -426,35 +461,90 @@ public abstract class BatteryStats implements Parcelable { /** * Returns the total number of times startService() has been called. * - * @param which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT. + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. */ public abstract int getStarts(int which); /** * Returns the total number times the service has been launched. * - * @param which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT. + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. */ public abstract int getLaunches(int which); } } } + 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 +554,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 +662,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); - } + wakelockTag = null; } - if (batteryLevelIntChanged) { - dest.writeInt(batteryLevelInt); - if (DEBUG) Slog.i(TAG, "WRITE DELTA: batteryToken=0x" - + Integer.toHexString(batteryLevelInt) - + " batteryLevel=" + batteryLevel - + " batteryTemp=" + batteryTemperature - + " batteryVolt=" + (int)batteryVoltage); - } - 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 +738,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 +765,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 +836,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 +900,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 +919,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 +932,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 +942,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 +967,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 +976,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 +1049,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 +1058,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 +1113,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 +1121,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 +1160,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; - public static final int NUM_NETWORK_ACTIVITY_TYPES = NETWORK_WIFI_TX_BYTES + 1; + static final String[] BLUETOOTH_STATE_NAMES = { + "inactive", "low", "med", "high" + }; - public abstract long getNetworkActivityCount(int type, int which); + 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); + + /** + * 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 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 +1224,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 +1255,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. */ @@ -1035,7 +1287,7 @@ public abstract class BatteryStats implements Parcelable { * Returns the total, last, or current battery uptime in microseconds. * * @param curTime the elapsed realtime in microseconds. - * @param which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT. + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. */ public abstract long computeBatteryUptime(long curTime, int which); @@ -1043,31 +1295,95 @@ public abstract class BatteryStats implements Parcelable { * Returns the total, last, or current battery realtime in microseconds. * * @param curTime the current elapsed realtime in microseconds. - * @param which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT. + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. */ 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_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, 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_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, 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. - * @param which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT. + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. */ public abstract long computeUptime(long curTime, int which); /** * Returns the total, last, or current realtime in microseconds. - * * + * * @param curTime the current elapsed realtime in microseconds. - * @param which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT. + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. */ public abstract long computeRealtime(long curTime, int which); - + + /** + * Compute an approximation for how much run time (in microseconds) is remaining on + * the battery. Returns -1 if no time can be computed: either there is not + * enough current data to make a decision, or the battery is currently + * charging. + * + * @param curTime The current elepsed realtime in microseconds. + */ + public abstract long computeBatteryTimeRemaining(long curTime); + + /** + * Return the historical number of discharge steps we currently have. + */ + public abstract int getNumDischargeStepDurations(); + + /** + * Return the array of discharge step durations; the number of valid + * items in it is returned by {@link #getNumDischargeStepDurations()}. + * These values are in milliseconds. + */ + public abstract long[] getDischargeStepDurationsArray(); + + /** + * Compute an approximation for how much time (in microseconds) remains until the battery + * is fully charged. Returns -1 if no time can be computed: either there is not + * enough current data to make a decision, or the battery is currently + * discharging. + * + * @param curTime The current elepsed realtime in microseconds. + */ + public abstract long computeChargeTimeRemaining(long curTime); + + /** + * Return the historical number of charge steps we currently have. + */ + public abstract int getNumChargeStepDurations(); + + /** + * Return the array of charge step durations; the number of valid + * items in it is returned by {@link #getNumChargeStepDurations()}. + * These values are in milliseconds. + */ + public abstract long[] getChargeStepDurationsArray(); + + 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 +1412,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 +1443,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 +1460,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 +1474,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 which which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, 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 +1508,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 which which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, 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 +1557,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 +1585,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 +1604,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 +1710,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, false); + 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 +1790,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 +1841,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 +1867,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 +1879,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 +1889,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 +1957,27 @@ 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); + final long batteryTimeRemaining = computeBatteryTimeRemaining(rawRealtime); + final long chargeTimeRemaining = computeChargeTimeRemaining(rawRealtime); + StringBuilder sb = new StringBuilder(128); SparseArray<? extends Uid> uidStats = getUidStats(); @@ -1556,37 +1995,69 @@ 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, "); - 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); + sb.append("uptime"); + if (batteryTimeRemaining >= 0) { + sb.setLength(0); + sb.append(prefix); + sb.append(" Battery time remaining: "); + formatTimeMs(sb, batteryTimeRemaining / 1000); + pw.println(sb.toString()); + } + if (chargeTimeRemaining >= 0) { + sb.setLength(0); + sb.append(prefix); + sb.append(" Charge time remaining: "); + formatTimeMs(sb, chargeTimeRemaining / 1000); + pw.println(sb.toString()); + } + 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 +2066,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 +2087,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 +2109,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)); + 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(" Total full wakelock time: "); formatTimeMs(sb, - (fullWakeLockTimeTotalMicros + 500) / 1000); - sb.append(", Total partial wakelock time: "); formatTimeMs(sb, - (partialWakeLockTimeTotalMicros + 500) / 1000); - pw.println(sb.toString()); - - 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 +2160,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 +2190,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 +2329,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, false); + 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 +2521,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 (wifiRxBytes > 0 || wifiTxBytes > 0) { + 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 || 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 +2608,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 +2621,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 +2635,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 +2695,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 +2719,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 +2738,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 +2860,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 +2914,95 @@ 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; + long firstTime = -1; + + public void printNextItem(PrintWriter pw, HistoryItem rec, long baseTime, boolean checkin, + boolean verbose) { + if (!checkin) { + pw.print(" "); + TimeUtils.formatDuration(rec.time - baseTime, pw, TimeUtils.HUNDRED_DAY_FIELD_LEN); + pw.print(" ("); + pw.print(rec.numReadInts); + pw.print(") "); + } else { + if (lastTime < 0) { + pw.print(rec.time - baseTime); + } 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 +3011,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 +3041,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 +3062,337 @@ 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"); + 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); + } + + private static boolean dumpDurationSteps(PrintWriter pw, String header, long[] steps, + int count, boolean checkin) { + if (count <= 0) { + return false; + } + if (!checkin) { + pw.println(header); + } + String[] lineArgs = new String[1]; + for (int i=0; i<count; i++) { + if (checkin) { + lineArgs[0] = Long.toString(steps[i]); + dumpLine(pw, 0 /* uid */, "i" /* category */, header, (Object[])lineArgs); } 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); + pw.print(" #"); pw.print(i); pw.print(": "); + TimeUtils.formatDuration(steps[i], pw); + pw.println(); } } + return true; } + 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; - final HistoryItem rec = new HistoryItem(); - if (startIteratingHistoryLocked()) { - pw.println("Battery History:"); - HistoryPrinter hprinter = new HistoryPrinter(); - while (getNextHistoryLocked(rec)) { - hprinter.printNextItem(pw, rec, now); + 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; + long baseTime = -1; + boolean printed = false; + while (getNextHistoryLocked(rec)) { + lastTime = rec.time; + if (baseTime < 0) { + baseTime = lastTime; + } + if (rec.time >= histStart) { + if (histStart >= 0 && !printed) { + if (rec.cmd == HistoryItem.CMD_CURRENT_TIME) { + printed = true; + } else if (rec.currentTime != 0) { + printed = true; + byte cmd = rec.cmd; + rec.cmd = HistoryItem.CMD_CURRENT_TIME; + hprinter.printNextItem(pw, rec, baseTime, false, + (flags&DUMP_VERBOSE) != 0); + rec.cmd = cmd; + } + } + hprinter.printNextItem(pw, rec, baseTime, false, + (flags&DUMP_VERBOSE) != 0); + } + } + if (histStart >= 0) { + pw.print(" NEXT: "); pw.println(lastTime+1); + } + pw.println(); + } finally { + finishIteratingHistoryLocked(); + } } - finishIteratingHistoryLocked(); - pw.println(""); - } - if (startIteratingOldHistoryLocked()) { - pw.println("Old battery History:"); - HistoryPrinter hprinter = new HistoryPrinter(); - while (getNextOldHistoryLocked(rec)) { - hprinter.printNextItem(pw, rec, now); + if (startIteratingOldHistoryLocked()) { + try { + pw.println("Old battery History:"); + HistoryPrinter hprinter = new HistoryPrinter(); + long baseTime = -1; + while (getNextOldHistoryLocked(rec)) { + if (baseTime < 0) { + baseTime = rec.time; + } + hprinter.printNextItem(pw, rec, baseTime, false, (flags&DUMP_VERBOSE) != 0); + } + pw.println(); + } finally { + finishIteratingOldHistoryLocked(); + } } - finishIteratingOldHistoryLocked(); - pw.println(""); } - - 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 && (flags&(DUMP_UNPLUGGED_ONLY|DUMP_CHARGED_ONLY)) == 0) { + return; + } + + 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 (dumpDurationSteps(pw, "Discharge step durations:", getDischargeStepDurationsArray(), + getNumDischargeStepDurations(), false)) { + long timeRemaining = computeBatteryTimeRemaining(SystemClock.elapsedRealtime()); + if (timeRemaining >= 0) { + pw.print(" Estimated discharge time remaining: "); + TimeUtils.formatDuration(timeRemaining / 1000, pw); + pw.println(); + } + pw.println(); + } + if (dumpDurationSteps(pw, "Charge step durations:", getChargeStepDurationsArray(), + getNumChargeStepDurations(), false)) { + long timeRemaining = computeChargeTimeRemaining(SystemClock.elapsedRealtime()); + if (timeRemaining >= 0) { + pw.print(" Estimated charge time remaining: "); + TimeUtils.formatDuration(timeRemaining / 1000, pw); + pw.println(); + } + 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); - pw.println(""); + dumpLocked(context, pw, "", STATS_SINCE_CHARGED, reqUid); + pw.println(); + } + if (!filtering || (flags&DUMP_UNPLUGGED_ONLY) != 0) { + pw.println("Statistics since last unplugged:"); + dumpLocked(context, pw, "", STATS_SINCE_UNPLUGGED, reqUid); } - pw.println("Statistics since last unplugged:"); - dumpLocked(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(getHistoryTagPoolUid(i)); + pw.print(",\""); + String str = getHistoryTagPoolString(i); + str = str.replace("\\", "\\\\"); + str = str.replace("\"", "\\\""); + pw.print(str); + pw.print("\""); + pw.println(); + } + HistoryPrinter hprinter = new HistoryPrinter(); + long lastTime = -1; + long baseTime = -1; + boolean printed = false; + while (getNextHistoryLocked(rec)) { + lastTime = rec.time; + if (baseTime < 0) { + baseTime = lastTime; + } + if (rec.time >= histStart) { + if (histStart >= 0 && !printed) { + if (rec.cmd == HistoryItem.CMD_CURRENT_TIME) { + printed = true; + } else if (rec.currentTime != 0) { + printed = true; + byte cmd = rec.cmd; + rec.cmd = HistoryItem.CMD_CURRENT_TIME; + pw.print(BATTERY_STATS_CHECKIN_VERSION); pw.print(','); + pw.print(HISTORY_DATA); pw.print(','); + hprinter.printNextItem(pw, rec, baseTime, true, false); + rec.cmd = cmd; + } + } + pw.print(BATTERY_STATS_CHECKIN_VERSION); pw.print(','); + pw.print(HISTORY_DATA); pw.print(','); + hprinter.printNextItem(pw, rec, baseTime, 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 +3420,17 @@ public abstract class BatteryStats implements Parcelable { } } } - if (isUnpluggedOnly) { - dumpCheckinLocked(pw, STATS_SINCE_UNPLUGGED, -1); + if (!filtering) { + dumpDurationSteps(pw, DISCHARGE_STEP_DATA, getDischargeStepDurationsArray(), + getNumDischargeStepDurations(), true); + dumpDurationSteps(pw, CHARGE_STEP_DATA, getChargeStepDurationsArray(), + getNumChargeStepDurations(), true); + } + 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 fc4fae2..1ca6b90 100644 --- a/core/java/android/os/Build.java +++ b/core/java/android/os/Build.java @@ -16,12 +16,17 @@ package android.os; +import android.text.TextUtils; +import android.util.Slog; + import com.android.internal.telephony.TelephonyProperties; /** * Information about the current build, extracted from system properties. */ public class Build { + private static final String TAG = "Build"; + /** Value used for when a build property is unknown. */ public static final String UNKNOWN = "unknown"; @@ -144,14 +149,22 @@ public class Build { */ public static final String CODENAME = getString("ro.build.version.codename"); + private static final String[] ALL_CODENAMES + = getString("ro.build.version.all_codenames").split(","); + + /** + * @hide + */ + public static final String[] ACTIVE_CODENAMES = "REL".equals(ALL_CODENAMES[0]) + ? new String[0] : ALL_CODENAMES; + /** * The SDK version to use when accessing resources. - * Use the current SDK version code. If we are a development build, - * also allow the previous SDK version + 1. + * Use the current SDK version code. For every active development codename + * we are operating under, we bump the assumed resource platform version by 1. * @hide */ - public static final int RESOURCES_SDK_INT = SDK_INT - + ("REL".equals(CODENAME) ? 0 : 1); + public static final int RESOURCES_SDK_INT = SDK_INT + ACTIVE_CODENAMES.length; } /** @@ -505,6 +518,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 implicit + * Intent.</li> + * </ul> + */ + public static final int L = CUR_DEVELOPMENT; } /** The type of build, like "user" or "eng". */ @@ -514,7 +540,43 @@ public class Build { public static final String TAGS = getString("ro.build.tags"); /** A string that uniquely identifies this build. Do not attempt to parse this value. */ - public static final String FINGERPRINT = getString("ro.build.fingerprint"); + public static final String FINGERPRINT = deriveFingerprint(); + + /** + * Some devices split the fingerprint components between multiple + * partitions, so we might derive the fingerprint at runtime. + */ + private static String deriveFingerprint() { + String finger = SystemProperties.get("ro.build.fingerprint"); + if (TextUtils.isEmpty(finger)) { + finger = getString("ro.product.brand") + '/' + + getString("ro.product.name") + '/' + + getString("ro.product.device") + ':' + + getString("ro.build.version.release") + '/' + + getString("ro.build.id") + '/' + + getString("ro.build.version.incremental") + ':' + + getString("ro.build.type") + '/' + + getString("ro.build.tags"); + } + return finger; + } + + /** + * Ensure that raw fingerprint system property is defined. If it was derived + * dynamically by {@link #deriveFingerprint()} this is where we push the + * derived value into the property service. + * + * @hide + */ + public static void ensureFingerprintProperty() { + if (TextUtils.isEmpty(SystemProperties.get("ro.build.fingerprint"))) { + try { + SystemProperties.set("ro.build.fingerprint", FINGERPRINT); + } catch (IllegalArgumentException e) { + Slog.e(TAG, "Failed to set fingerprint property", e); + } + } + } // The following properties only make sense for internal engineering builds. public static final long TIME = getLong("ro.build.date.utc") * 1000; diff --git a/core/java/android/os/Bundle.java b/core/java/android/os/Bundle.java index af57507..c85e418 100644 --- a/core/java/android/os/Bundle.java +++ b/core/java/android/os/Bundle.java @@ -17,7 +17,6 @@ package android.os; import android.util.ArrayMap; -import android.util.Log; import android.util.SparseArray; import java.io.Serializable; @@ -29,47 +28,25 @@ import java.util.Set; * A mapping from String values to various Parcelable types. * */ -public final class Bundle implements Parcelable, Cloneable { - private static final String TAG = "Bundle"; - static final boolean DEBUG = false; +public final class Bundle extends CommonBundle { public static final Bundle EMPTY; - - static final int BUNDLE_MAGIC = 0x4C444E42; // 'B' 'N' 'D' 'L' static final Parcel EMPTY_PARCEL; static { EMPTY = new Bundle(); EMPTY.mMap = ArrayMap.EMPTY; - EMPTY_PARCEL = Parcel.obtain(); + EMPTY_PARCEL = CommonBundle.EMPTY_PARCEL; } - // Invariant - exactly one of mMap / mParcelledData will be null - // (except inside a call to unparcel) - - /* package */ ArrayMap<String, Object> mMap = null; - - /* - * If mParcelledData is non-null, then mMap will be null and the - * data are stored as a Parcel containing a Bundle. When the data - * are unparcelled, mParcelledData willbe set to null. - */ - /* package */ Parcel mParcelledData = null; - private boolean mHasFds = false; private boolean mFdsKnown = true; private boolean mAllowFds = true; /** - * The ClassLoader used when unparcelling data from mParcelledData. - */ - private ClassLoader mClassLoader; - - /** * Constructs a new, empty Bundle. */ public Bundle() { - mMap = new ArrayMap<String, Object>(); - mClassLoader = getClass().getClassLoader(); + super(); } /** @@ -79,11 +56,17 @@ public final class Bundle implements Parcelable, Cloneable { * @param parcelledData a Parcel containing a Bundle */ Bundle(Parcel parcelledData) { - readFromParcel(parcelledData); + super(parcelledData); + + mHasFds = mParcelledData.hasFileDescriptors(); + mFdsKnown = true; } /* package */ Bundle(Parcel parcelledData, int length) { - readFromParcelInner(parcelledData, length); + super(parcelledData, length); + + mHasFds = mParcelledData.hasFileDescriptors(); + mFdsKnown = true; } /** @@ -94,8 +77,7 @@ public final class Bundle implements Parcelable, Cloneable { * inside of the Bundle. */ public Bundle(ClassLoader loader) { - mMap = new ArrayMap<String, Object>(); - mClassLoader = loader; + super(loader); } /** @@ -105,8 +87,7 @@ public final class Bundle implements Parcelable, Cloneable { * @param capacity the initial capacity of the Bundle */ public Bundle(int capacity) { - mMap = new ArrayMap<String, Object>(capacity); - mClassLoader = getClass().getClassLoader(); + super(capacity); } /** @@ -116,27 +97,20 @@ public final class Bundle implements Parcelable, Cloneable { * @param b a Bundle to be copied. */ public Bundle(Bundle b) { - if (b.mParcelledData != null) { - if (b.mParcelledData == EMPTY_PARCEL) { - mParcelledData = EMPTY_PARCEL; - } else { - mParcelledData = Parcel.obtain(); - mParcelledData.appendFrom(b.mParcelledData, 0, b.mParcelledData.dataSize()); - mParcelledData.setDataPosition(0); - } - } else { - mParcelledData = null; - } - - if (b.mMap != null) { - mMap = new ArrayMap<String, Object>(b.mMap); - } else { - mMap = null; - } + super(b); mHasFds = b.mHasFds; mFdsKnown = b.mFdsKnown; - mClassLoader = b.mClassLoader; + } + + /** + * Constructs a Bundle containing a copy of the mappings from the given + * PersistableBundle. + * + * @param b a Bundle to be copied. + */ + public Bundle(PersistableBundle b) { + super(b); } /** @@ -145,37 +119,17 @@ public final class Bundle implements Parcelable, Cloneable { * @hide */ public static Bundle forPair(String key, String value) { - // TODO: optimize this case. Bundle b = new Bundle(1); b.putString(key, value); return b; } /** - * TODO: optimize this later (getting just the value part of a Bundle - * with a single pair) once Bundle.forPair() above is implemented - * with a special single-value Map implementation/serialization. - * - * Note: value in single-pair Bundle may be null. - * * @hide */ + @Override public String getPairValue() { - unparcel(); - int size = mMap.size(); - if (size > 1) { - Log.w(TAG, "getPairValue() used on Bundle with multiple pairs."); - } - if (size == 0) { - return null; - } - Object o = mMap.valueAt(0); - try { - return (String) o; - } catch (ClassCastException e) { - typeWarning("getPairValue()", o, "String", e); - return null; - } + return super.getPairValue(); } /** @@ -184,15 +138,17 @@ public final class Bundle implements Parcelable, Cloneable { * @param loader An explicit ClassLoader to use when instantiating objects * inside of the Bundle. */ + @Override public void setClassLoader(ClassLoader loader) { - mClassLoader = loader; + super.setClassLoader(loader); } /** * Return the ClassLoader currently associated with this Bundle. */ + @Override public ClassLoader getClassLoader() { - return mClassLoader; + return super.getClassLoader(); } /** @hide */ @@ -212,52 +168,11 @@ public final class Bundle implements Parcelable, Cloneable { } /** - * If the underlying data are stored as a Parcel, unparcel them - * using the currently assigned class loader. - */ - /* package */ synchronized void unparcel() { - if (mParcelledData == null) { - if (DEBUG) Log.d(TAG, "unparcel " + Integer.toHexString(System.identityHashCode(this)) - + ": no parcelled data"); - return; - } - - if (mParcelledData == EMPTY_PARCEL) { - if (DEBUG) Log.d(TAG, "unparcel " + Integer.toHexString(System.identityHashCode(this)) - + ": empty"); - if (mMap == null) { - mMap = new ArrayMap<String, Object>(1); - } else { - mMap.erase(); - } - mParcelledData = null; - return; - } - - int N = mParcelledData.readInt(); - if (DEBUG) Log.d(TAG, "unparcel " + Integer.toHexString(System.identityHashCode(this)) - + ": reading " + N + " maps"); - if (N < 0) { - return; - } - if (mMap == null) { - mMap = new ArrayMap<String, Object>(N); - } else { - mMap.erase(); - mMap.ensureCapacity(N); - } - mParcelledData.readArrayMapInternal(mMap, N, mClassLoader); - mParcelledData.recycle(); - mParcelledData = null; - if (DEBUG) Log.d(TAG, "unparcel " + Integer.toHexString(System.identityHashCode(this)) - + " final map: " + mMap); - } - - /** * @hide */ + @Override public boolean isParcelled() { - return mParcelledData != null; + return super.isParcelled(); } /** @@ -265,25 +180,26 @@ public final class Bundle implements Parcelable, Cloneable { * * @return the number of mappings as an int. */ + @Override public int size() { - unparcel(); - return mMap.size(); + return super.size(); } /** * Returns true if the mapping of this Bundle is empty, false otherwise. */ + @Override public boolean isEmpty() { - unparcel(); - return mMap.isEmpty(); + return super.isEmpty(); } /** * Removes all elements from the mapping of this Bundle. */ + @Override public void clear() { - unparcel(); - mMap.clear(); + super.clear(); + mHasFds = false; mFdsKnown = true; } @@ -295,9 +211,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String key * @return true if the key is part of the mapping, false otherwise */ + @Override public boolean containsKey(String key) { - unparcel(); - return mMap.containsKey(key); + return super.containsKey(key); } /** @@ -306,9 +222,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String key * @return an Object, or null */ + @Override public Object get(String key) { - unparcel(); - return mMap.get(key); + return super.get(key); } /** @@ -316,24 +232,33 @@ public final class Bundle implements Parcelable, Cloneable { * * @param key a String key */ + @Override public void remove(String key) { - unparcel(); - mMap.remove(key); + super.remove(key); } /** * Inserts all mappings from the given Bundle into this Bundle. * - * @param map a Bundle + * @param bundle a Bundle */ - public void putAll(Bundle map) { + public void putAll(Bundle bundle) { unparcel(); - map.unparcel(); - mMap.putAll(map.mMap); + bundle.unparcel(); + mMap.putAll(bundle.mMap); // fd state is now known if and only if both bundles already knew - mHasFds |= map.mHasFds; - mFdsKnown = mFdsKnown && map.mFdsKnown; + mHasFds |= bundle.mHasFds; + mFdsKnown = mFdsKnown && bundle.mFdsKnown; + } + + /** + * Inserts all mappings from the given PersistableBundle into this Bundle. + * + * @param bundle a PersistableBundle + */ + public void putAll(PersistableBundle bundle) { + super.putAll(bundle); } /** @@ -341,9 +266,9 @@ public final class Bundle implements Parcelable, Cloneable { * * @return a Set of String keys */ + @Override public Set<String> keySet() { - unparcel(); - return mMap.keySet(); + return super.keySet(); } /** @@ -352,7 +277,7 @@ public final class Bundle implements Parcelable, Cloneable { public boolean hasFileDescriptors() { if (!mFdsKnown) { boolean fdFound = false; // keep going until we find one or run out of data - + if (mParcelledData != null) { if (mParcelledData.hasFileDescriptors()) { fdFound = true; @@ -390,8 +315,7 @@ public final class Bundle implements Parcelable, Cloneable { ArrayList array = (ArrayList) obj; // an ArrayList here might contain either Strings or // Parcelables; only look inside for Parcelables - if ((array.size() > 0) - && (array.get(0) instanceof Parcelable)) { + if (!array.isEmpty() && (array.get(0) instanceof Parcelable)) { for (int n = array.size() - 1; n >= 0; n--) { Parcelable p = (Parcelable) array.get(n); if (p != null && ((p.describeContents() @@ -410,7 +334,7 @@ public final class Bundle implements Parcelable, Cloneable { } return mHasFds; } - + /** * Inserts a Boolean value into the mapping of this Bundle, replacing * any existing value for the given key. Either key or value may be null. @@ -418,9 +342,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @param value a Boolean, or null */ + @Override public void putBoolean(String key, boolean value) { - unparcel(); - mMap.put(key, value); + super.putBoolean(key, value); } /** @@ -430,9 +354,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @param value a byte */ + @Override public void putByte(String key, byte value) { - unparcel(); - mMap.put(key, value); + super.putByte(key, value); } /** @@ -442,9 +366,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @param value a char, or null */ + @Override public void putChar(String key, char value) { - unparcel(); - mMap.put(key, value); + super.putChar(key, value); } /** @@ -454,9 +378,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @param value a short */ + @Override public void putShort(String key, short value) { - unparcel(); - mMap.put(key, value); + super.putShort(key, value); } /** @@ -466,9 +390,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @param value an int, or null */ + @Override public void putInt(String key, int value) { - unparcel(); - mMap.put(key, value); + super.putInt(key, value); } /** @@ -478,9 +402,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @param value a long */ + @Override public void putLong(String key, long value) { - unparcel(); - mMap.put(key, value); + super.putLong(key, value); } /** @@ -490,9 +414,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @param value a float */ + @Override public void putFloat(String key, float value) { - unparcel(); - mMap.put(key, value); + super.putFloat(key, value); } /** @@ -502,9 +426,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @param value a double */ + @Override public void putDouble(String key, double value) { - unparcel(); - mMap.put(key, value); + super.putDouble(key, value); } /** @@ -514,9 +438,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @param value a String, or null */ + @Override public void putString(String key, String value) { - unparcel(); - mMap.put(key, value); + super.putString(key, value); } /** @@ -526,9 +450,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @param value a CharSequence, or null */ + @Override public void putCharSequence(String key, CharSequence value) { - unparcel(); - mMap.put(key, value); + super.putCharSequence(key, value); } /** @@ -567,7 +491,7 @@ public final class Bundle implements Parcelable, Cloneable { * @param value an ArrayList of Parcelable objects, or null */ public void putParcelableArrayList(String key, - ArrayList<? extends Parcelable> value) { + ArrayList<? extends Parcelable> value) { unparcel(); mMap.put(key, value); mFdsKnown = false; @@ -602,9 +526,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @param value an ArrayList<Integer> object, or null */ + @Override public void putIntegerArrayList(String key, ArrayList<Integer> value) { - unparcel(); - mMap.put(key, value); + super.putIntegerArrayList(key, value); } /** @@ -614,9 +538,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @param value an ArrayList<String> object, or null */ + @Override public void putStringArrayList(String key, ArrayList<String> value) { - unparcel(); - mMap.put(key, value); + super.putStringArrayList(key, value); } /** @@ -626,9 +550,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @param value an ArrayList<CharSequence> object, or null */ + @Override public void putCharSequenceArrayList(String key, ArrayList<CharSequence> value) { - unparcel(); - mMap.put(key, value); + super.putCharSequenceArrayList(key, value); } /** @@ -638,9 +562,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @param value a Serializable object, or null */ + @Override public void putSerializable(String key, Serializable value) { - unparcel(); - mMap.put(key, value); + super.putSerializable(key, value); } /** @@ -650,9 +574,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @param value a boolean array object, or null */ + @Override public void putBooleanArray(String key, boolean[] value) { - unparcel(); - mMap.put(key, value); + super.putBooleanArray(key, value); } /** @@ -662,9 +586,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @param value a byte array object, or null */ + @Override public void putByteArray(String key, byte[] value) { - unparcel(); - mMap.put(key, value); + super.putByteArray(key, value); } /** @@ -674,9 +598,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @param value a short array object, or null */ + @Override public void putShortArray(String key, short[] value) { - unparcel(); - mMap.put(key, value); + super.putShortArray(key, value); } /** @@ -686,9 +610,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @param value a char array object, or null */ + @Override public void putCharArray(String key, char[] value) { - unparcel(); - mMap.put(key, value); + super.putCharArray(key, value); } /** @@ -698,9 +622,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @param value an int array object, or null */ + @Override public void putIntArray(String key, int[] value) { - unparcel(); - mMap.put(key, value); + super.putIntArray(key, value); } /** @@ -710,9 +634,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @param value a long array object, or null */ + @Override public void putLongArray(String key, long[] value) { - unparcel(); - mMap.put(key, value); + super.putLongArray(key, value); } /** @@ -722,9 +646,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @param value a float array object, or null */ + @Override public void putFloatArray(String key, float[] value) { - unparcel(); - mMap.put(key, value); + super.putFloatArray(key, value); } /** @@ -734,9 +658,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @param value a double array object, or null */ + @Override public void putDoubleArray(String key, double[] value) { - unparcel(); - mMap.put(key, value); + super.putDoubleArray(key, value); } /** @@ -746,9 +670,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @param value a String array object, or null */ + @Override public void putStringArray(String key, String[] value) { - unparcel(); - mMap.put(key, value); + super.putStringArray(key, value); } /** @@ -758,9 +682,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @param value a CharSequence array object, or null */ + @Override public void putCharSequenceArray(String key, CharSequence[] value) { - unparcel(); - mMap.put(key, value); + super.putCharSequenceArray(key, value); } /** @@ -776,6 +700,17 @@ public final class Bundle implements Parcelable, Cloneable { } /** + * Inserts a PersistableBundle value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a Bundle object, or null + */ + public void putPersistableBundle(String key, PersistableBundle value) { + super.putPersistableBundle(key, value); + } + + /** * Inserts an {@link IBinder} value into the mapping of this Bundle, replacing * any existing value for the given key. Either key or value may be null. * @@ -817,33 +752,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String * @return a boolean value */ + @Override public boolean getBoolean(String key) { - unparcel(); - if (DEBUG) Log.d(TAG, "Getting boolean in " - + Integer.toHexString(System.identityHashCode(this))); - return getBoolean(key, false); - } - - // Log a message if the value was non-null but not of the expected type - private void typeWarning(String key, Object value, String className, - Object defaultValue, ClassCastException e) { - StringBuilder sb = new StringBuilder(); - sb.append("Key "); - sb.append(key); - sb.append(" expected "); - sb.append(className); - sb.append(" but value was a "); - sb.append(value.getClass().getName()); - sb.append(". The default value "); - sb.append(defaultValue); - sb.append(" was returned."); - Log.w(TAG, sb.toString()); - Log.w(TAG, "Attempt to cast generated internal exception:", e); - } - - private void typeWarning(String key, Object value, String className, - ClassCastException e) { - typeWarning(key, value, className, "<null>", e); + return super.getBoolean(key); } /** @@ -854,18 +765,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param defaultValue Value to return if key does not exist * @return a boolean value */ + @Override public boolean getBoolean(String key, boolean defaultValue) { - unparcel(); - Object o = mMap.get(key); - if (o == null) { - return defaultValue; - } - try { - return (Boolean) o; - } catch (ClassCastException e) { - typeWarning(key, o, "Boolean", defaultValue, e); - return defaultValue; - } + return super.getBoolean(key, defaultValue); } /** @@ -875,9 +777,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String * @return a byte value */ + @Override public byte getByte(String key) { - unparcel(); - return getByte(key, (byte) 0); + return super.getByte(key); } /** @@ -888,18 +790,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param defaultValue Value to return if key does not exist * @return a byte value */ + @Override public Byte getByte(String key, byte defaultValue) { - unparcel(); - Object o = mMap.get(key); - if (o == null) { - return defaultValue; - } - try { - return (Byte) o; - } catch (ClassCastException e) { - typeWarning(key, o, "Byte", defaultValue, e); - return defaultValue; - } + return super.getByte(key, defaultValue); } /** @@ -909,9 +802,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String * @return a char value */ + @Override public char getChar(String key) { - unparcel(); - return getChar(key, (char) 0); + return super.getChar(key); } /** @@ -922,18 +815,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param defaultValue Value to return if key does not exist * @return a char value */ + @Override public char getChar(String key, char defaultValue) { - unparcel(); - Object o = mMap.get(key); - if (o == null) { - return defaultValue; - } - try { - return (Character) o; - } catch (ClassCastException e) { - typeWarning(key, o, "Character", defaultValue, e); - return defaultValue; - } + return super.getChar(key, defaultValue); } /** @@ -943,9 +827,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String * @return a short value */ + @Override public short getShort(String key) { - unparcel(); - return getShort(key, (short) 0); + return super.getShort(key); } /** @@ -956,18 +840,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param defaultValue Value to return if key does not exist * @return a short value */ + @Override public short getShort(String key, short defaultValue) { - unparcel(); - Object o = mMap.get(key); - if (o == null) { - return defaultValue; - } - try { - return (Short) o; - } catch (ClassCastException e) { - typeWarning(key, o, "Short", defaultValue, e); - return defaultValue; - } + return super.getShort(key, defaultValue); } /** @@ -977,9 +852,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String * @return an int value */ + @Override public int getInt(String key) { - unparcel(); - return getInt(key, 0); + return super.getInt(key); } /** @@ -990,18 +865,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param defaultValue Value to return if key does not exist * @return an int value */ + @Override public int getInt(String key, int defaultValue) { - unparcel(); - Object o = mMap.get(key); - if (o == null) { - return defaultValue; - } - try { - return (Integer) o; - } catch (ClassCastException e) { - typeWarning(key, o, "Integer", defaultValue, e); - return defaultValue; - } + return super.getInt(key, defaultValue); } /** @@ -1011,9 +877,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String * @return a long value */ + @Override public long getLong(String key) { - unparcel(); - return getLong(key, 0L); + return super.getLong(key); } /** @@ -1024,18 +890,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param defaultValue Value to return if key does not exist * @return a long value */ + @Override public long getLong(String key, long defaultValue) { - unparcel(); - Object o = mMap.get(key); - if (o == null) { - return defaultValue; - } - try { - return (Long) o; - } catch (ClassCastException e) { - typeWarning(key, o, "Long", defaultValue, e); - return defaultValue; - } + return super.getLong(key, defaultValue); } /** @@ -1045,9 +902,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String * @return a float value */ + @Override public float getFloat(String key) { - unparcel(); - return getFloat(key, 0.0f); + return super.getFloat(key); } /** @@ -1058,18 +915,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param defaultValue Value to return if key does not exist * @return a float value */ + @Override public float getFloat(String key, float defaultValue) { - unparcel(); - Object o = mMap.get(key); - if (o == null) { - return defaultValue; - } - try { - return (Float) o; - } catch (ClassCastException e) { - typeWarning(key, o, "Float", defaultValue, e); - return defaultValue; - } + return super.getFloat(key, defaultValue); } /** @@ -1079,9 +927,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String * @return a double value */ + @Override public double getDouble(String key) { - unparcel(); - return getDouble(key, 0.0); + return super.getDouble(key); } /** @@ -1092,18 +940,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param defaultValue Value to return if key does not exist * @return a double value */ + @Override public double getDouble(String key, double defaultValue) { - unparcel(); - Object o = mMap.get(key); - if (o == null) { - return defaultValue; - } - try { - return (Double) o; - } catch (ClassCastException e) { - typeWarning(key, o, "Double", defaultValue, e); - return defaultValue; - } + return super.getDouble(key, defaultValue); } /** @@ -1114,15 +953,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @return a String value, or null */ + @Override public String getString(String key) { - unparcel(); - final Object o = mMap.get(key); - try { - return (String) o; - } catch (ClassCastException e) { - typeWarning(key, o, "String", e); - return null; - } + return super.getString(key); } /** @@ -1134,9 +967,9 @@ public final class Bundle implements Parcelable, Cloneable { * @return the String value associated with the given key, or defaultValue * if no valid String object is currently mapped to that key. */ + @Override public String getString(String key, String defaultValue) { - final String s = getString(key); - return (s == null) ? defaultValue : s; + return super.getString(key, defaultValue); } /** @@ -1147,15 +980,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @return a CharSequence value, or null */ + @Override public CharSequence getCharSequence(String key) { - unparcel(); - final Object o = mMap.get(key); - try { - return (CharSequence) o; - } catch (ClassCastException e) { - typeWarning(key, o, "CharSequence", e); - return null; - } + return super.getCharSequence(key); } /** @@ -1167,9 +994,9 @@ public final class Bundle implements Parcelable, Cloneable { * @return the CharSequence value associated with the given key, or defaultValue * if no valid CharSequence object is currently mapped to that key. */ + @Override public CharSequence getCharSequence(String key, CharSequence defaultValue) { - final CharSequence cs = getCharSequence(key); - return (cs == null) ? defaultValue : cs; + return super.getCharSequence(key, defaultValue); } /** @@ -1200,6 +1027,18 @@ public final class Bundle implements Parcelable, Cloneable { * value is explicitly associated with the key. * * @param key a String, or null + * @return a PersistableBundle value, or null + */ + public PersistableBundle getPersistableBundle(String key) { + return super.getPersistableBundle(key); + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null * @return a Parcelable value, or null */ public <T extends Parcelable> T getParcelable(String key) { @@ -1291,18 +1130,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @return a Serializable value, or null */ + @Override public Serializable getSerializable(String key) { - unparcel(); - Object o = mMap.get(key); - if (o == null) { - return null; - } - try { - return (Serializable) o; - } catch (ClassCastException e) { - typeWarning(key, o, "Serializable", e); - return null; - } + return super.getSerializable(key); } /** @@ -1313,18 +1143,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @return an ArrayList<String> value, or null */ + @Override public ArrayList<Integer> getIntegerArrayList(String key) { - unparcel(); - Object o = mMap.get(key); - if (o == null) { - return null; - } - try { - return (ArrayList<Integer>) o; - } catch (ClassCastException e) { - typeWarning(key, o, "ArrayList<Integer>", e); - return null; - } + return super.getIntegerArrayList(key); } /** @@ -1335,18 +1156,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @return an ArrayList<String> value, or null */ + @Override public ArrayList<String> getStringArrayList(String key) { - unparcel(); - Object o = mMap.get(key); - if (o == null) { - return null; - } - try { - return (ArrayList<String>) o; - } catch (ClassCastException e) { - typeWarning(key, o, "ArrayList<String>", e); - return null; - } + return super.getStringArrayList(key); } /** @@ -1357,18 +1169,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @return an ArrayList<CharSequence> value, or null */ + @Override public ArrayList<CharSequence> getCharSequenceArrayList(String key) { - unparcel(); - Object o = mMap.get(key); - if (o == null) { - return null; - } - try { - return (ArrayList<CharSequence>) o; - } catch (ClassCastException e) { - typeWarning(key, o, "ArrayList<CharSequence>", e); - return null; - } + return super.getCharSequenceArrayList(key); } /** @@ -1379,18 +1182,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @return a boolean[] value, or null */ + @Override public boolean[] getBooleanArray(String key) { - unparcel(); - Object o = mMap.get(key); - if (o == null) { - return null; - } - try { - return (boolean[]) o; - } catch (ClassCastException e) { - typeWarning(key, o, "byte[]", e); - return null; - } + return super.getBooleanArray(key); } /** @@ -1401,18 +1195,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @return a byte[] value, or null */ + @Override public byte[] getByteArray(String key) { - unparcel(); - Object o = mMap.get(key); - if (o == null) { - return null; - } - try { - return (byte[]) o; - } catch (ClassCastException e) { - typeWarning(key, o, "byte[]", e); - return null; - } + return super.getByteArray(key); } /** @@ -1423,18 +1208,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @return a short[] value, or null */ + @Override public short[] getShortArray(String key) { - unparcel(); - Object o = mMap.get(key); - if (o == null) { - return null; - } - try { - return (short[]) o; - } catch (ClassCastException e) { - typeWarning(key, o, "short[]", e); - return null; - } + return super.getShortArray(key); } /** @@ -1445,18 +1221,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @return a char[] value, or null */ + @Override public char[] getCharArray(String key) { - unparcel(); - Object o = mMap.get(key); - if (o == null) { - return null; - } - try { - return (char[]) o; - } catch (ClassCastException e) { - typeWarning(key, o, "char[]", e); - return null; - } + return super.getCharArray(key); } /** @@ -1467,18 +1234,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @return an int[] value, or null */ + @Override public int[] getIntArray(String key) { - unparcel(); - Object o = mMap.get(key); - if (o == null) { - return null; - } - try { - return (int[]) o; - } catch (ClassCastException e) { - typeWarning(key, o, "int[]", e); - return null; - } + return super.getIntArray(key); } /** @@ -1489,18 +1247,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @return a long[] value, or null */ + @Override public long[] getLongArray(String key) { - unparcel(); - Object o = mMap.get(key); - if (o == null) { - return null; - } - try { - return (long[]) o; - } catch (ClassCastException e) { - typeWarning(key, o, "long[]", e); - return null; - } + return super.getLongArray(key); } /** @@ -1511,18 +1260,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @return a float[] value, or null */ + @Override public float[] getFloatArray(String key) { - unparcel(); - Object o = mMap.get(key); - if (o == null) { - return null; - } - try { - return (float[]) o; - } catch (ClassCastException e) { - typeWarning(key, o, "float[]", e); - return null; - } + return super.getFloatArray(key); } /** @@ -1533,18 +1273,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @return a double[] value, or null */ + @Override public double[] getDoubleArray(String key) { - unparcel(); - Object o = mMap.get(key); - if (o == null) { - return null; - } - try { - return (double[]) o; - } catch (ClassCastException e) { - typeWarning(key, o, "double[]", e); - return null; - } + return super.getDoubleArray(key); } /** @@ -1555,18 +1286,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @return a String[] value, or null */ + @Override public String[] getStringArray(String key) { - unparcel(); - Object o = mMap.get(key); - if (o == null) { - return null; - } - try { - return (String[]) o; - } catch (ClassCastException e) { - typeWarning(key, o, "String[]", e); - return null; - } + return super.getStringArray(key); } /** @@ -1577,18 +1299,9 @@ public final class Bundle implements Parcelable, Cloneable { * @param key a String, or null * @return a CharSequence[] value, or null */ + @Override public CharSequence[] getCharSequenceArray(String key) { - unparcel(); - Object o = mMap.get(key); - if (o == null) { - return null; - } - try { - return (CharSequence[]) o; - } catch (ClassCastException e) { - typeWarning(key, o, "CharSequence[]", e); - return null; - } + return super.getCharSequenceArray(key); } /** @@ -1641,10 +1354,12 @@ public final class Bundle implements Parcelable, Cloneable { public static final Parcelable.Creator<Bundle> CREATOR = new Parcelable.Creator<Bundle>() { + @Override public Bundle createFromParcel(Parcel in) { return in.readBundle(); } + @Override public Bundle[] newArray(int size) { return new Bundle[size]; } @@ -1653,6 +1368,7 @@ public final class Bundle implements Parcelable, Cloneable { /** * Report the nature of this Parcelable's contents */ + @Override public int describeContents() { int mask = 0; if (hasFileDescriptors()) { @@ -1660,44 +1376,17 @@ public final class Bundle implements Parcelable, Cloneable { } return mask; } - + /** * Writes the Bundle contents to a Parcel, typically in order for * it to be passed through an IBinder connection. * @param parcel The parcel to copy this bundle to. */ + @Override public void writeToParcel(Parcel parcel, int flags) { final boolean oldAllowFds = parcel.pushAllowFds(mAllowFds); try { - if (mParcelledData != null) { - if (mParcelledData == EMPTY_PARCEL) { - parcel.writeInt(0); - } else { - int length = mParcelledData.dataSize(); - parcel.writeInt(length); - parcel.writeInt(BUNDLE_MAGIC); - parcel.appendFrom(mParcelledData, 0, length); - } - } else { - // Special case for empty bundles. - if (mMap == null || mMap.size() <= 0) { - parcel.writeInt(0); - return; - } - int lengthPos = parcel.dataPosition(); - parcel.writeInt(-1); // dummy, will hold length - parcel.writeInt(BUNDLE_MAGIC); - - int startPos = parcel.dataPosition(); - parcel.writeArrayMapInternal(mMap); - int endPos = parcel.dataPosition(); - - // Backpatch length - parcel.setDataPosition(lengthPos); - int length = endPos - startPos; - parcel.writeInt(length); - parcel.setDataPosition(endPos); - } + super.writeToParcelInner(parcel, flags); } finally { parcel.restoreAllowFds(oldAllowFds); } @@ -1709,41 +1398,8 @@ public final class Bundle implements Parcelable, Cloneable { * @param parcel The parcel to overwrite this bundle from. */ public void readFromParcel(Parcel parcel) { - int length = parcel.readInt(); - if (length < 0) { - throw new RuntimeException("Bad length in parcel: " + length); - } - readFromParcelInner(parcel, length); - } - - void readFromParcelInner(Parcel parcel, int length) { - if (length == 0) { - // Empty Bundle or end of data. - mParcelledData = EMPTY_PARCEL; - mHasFds = false; - mFdsKnown = true; - return; - } - int magic = parcel.readInt(); - if (magic != BUNDLE_MAGIC) { - //noinspection ThrowableInstanceNeverThrown - throw new IllegalStateException("Bad magic number for Bundle: 0x" - + Integer.toHexString(magic)); - } - - // Advance within this Parcel - int offset = parcel.dataPosition(); - parcel.setDataPosition(offset + length); - - Parcel p = Parcel.obtain(); - p.setDataPosition(0); - p.appendFrom(parcel, offset, length); - if (DEBUG) Log.d(TAG, "Retrieving " + Integer.toHexString(System.identityHashCode(this)) - + ": " + length + " bundle bytes starting at " + offset); - p.setDataPosition(0); - - mParcelledData = p; - mHasFds = p.hasFileDescriptors(); + super.readFromParcelInner(parcel); + mHasFds = mParcelledData.hasFileDescriptors(); mFdsKnown = true; } @@ -1759,4 +1415,5 @@ public final class Bundle implements Parcelable, Cloneable { } return "Bundle[" + mMap.toString() + "]"; } + } diff --git a/core/java/android/os/CommonBundle.java b/core/java/android/os/CommonBundle.java new file mode 100644 index 0000000..e11f170 --- /dev/null +++ b/core/java/android/os/CommonBundle.java @@ -0,0 +1,1384 @@ +/* + * 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.os; + +import android.util.ArrayMap; +import android.util.Log; +import android.util.SparseArray; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * A mapping from String values to various types. + */ +abstract class CommonBundle implements Parcelable, Cloneable { + private static final String TAG = "Bundle"; + static final boolean DEBUG = false; + + static final int BUNDLE_MAGIC = 0x4C444E42; // 'B' 'N' 'D' 'L' + static final Parcel EMPTY_PARCEL; + + static { + EMPTY_PARCEL = Parcel.obtain(); + } + + // Invariant - exactly one of mMap / mParcelledData will be null + // (except inside a call to unparcel) + + ArrayMap<String, Object> mMap = null; + + /* + * If mParcelledData is non-null, then mMap will be null and the + * data are stored as a Parcel containing a Bundle. When the data + * are unparcelled, mParcelledData willbe set to null. + */ + Parcel mParcelledData = null; + + /** + * The ClassLoader used when unparcelling data from mParcelledData. + */ + private ClassLoader mClassLoader; + + /** + * Constructs a new, empty Bundle that uses a specific ClassLoader for + * instantiating Parcelable and Serializable objects. + * + * @param loader An explicit ClassLoader to use when instantiating objects + * inside of the Bundle. + * @param capacity Initial size of the ArrayMap. + */ + CommonBundle(ClassLoader loader, int capacity) { + mMap = capacity > 0 ? + new ArrayMap<String, Object>(capacity) : new ArrayMap<String, Object>(); + mClassLoader = loader == null ? getClass().getClassLoader() : loader; + } + + /** + * Constructs a new, empty Bundle. + */ + CommonBundle() { + this((ClassLoader) null, 0); + } + + /** + * Constructs a Bundle whose data is stored as a Parcel. The data + * will be unparcelled on first contact, using the assigned ClassLoader. + * + * @param parcelledData a Parcel containing a Bundle + */ + CommonBundle(Parcel parcelledData) { + readFromParcelInner(parcelledData); + } + + CommonBundle(Parcel parcelledData, int length) { + readFromParcelInner(parcelledData, length); + } + + /** + * Constructs a new, empty Bundle that uses a specific ClassLoader for + * instantiating Parcelable and Serializable objects. + * + * @param loader An explicit ClassLoader to use when instantiating objects + * inside of the Bundle. + */ + CommonBundle(ClassLoader loader) { + this(loader, 0); + } + + /** + * Constructs a new, empty Bundle sized to hold the given number of + * elements. The Bundle will grow as needed. + * + * @param capacity the initial capacity of the Bundle + */ + CommonBundle(int capacity) { + this((ClassLoader) null, capacity); + } + + /** + * Constructs a Bundle containing a copy of the mappings from the given + * Bundle. + * + * @param b a Bundle to be copied. + */ + CommonBundle(CommonBundle b) { + if (b.mParcelledData != null) { + if (b.mParcelledData == EMPTY_PARCEL) { + mParcelledData = EMPTY_PARCEL; + } else { + mParcelledData = Parcel.obtain(); + mParcelledData.appendFrom(b.mParcelledData, 0, b.mParcelledData.dataSize()); + mParcelledData.setDataPosition(0); + } + } else { + mParcelledData = null; + } + + if (b.mMap != null) { + mMap = new ArrayMap<String, Object>(b.mMap); + } else { + mMap = null; + } + + mClassLoader = b.mClassLoader; + } + + /** + * TODO: optimize this later (getting just the value part of a Bundle + * with a single pair) once Bundle.forPair() above is implemented + * with a special single-value Map implementation/serialization. + * + * Note: value in single-pair Bundle may be null. + * + * @hide + */ + String getPairValue() { + unparcel(); + int size = mMap.size(); + if (size > 1) { + Log.w(TAG, "getPairValue() used on Bundle with multiple pairs."); + } + if (size == 0) { + return null; + } + Object o = mMap.valueAt(0); + try { + return (String) o; + } catch (ClassCastException e) { + typeWarning("getPairValue()", o, "String", e); + return null; + } + } + + /** + * Changes the ClassLoader this Bundle uses when instantiating objects. + * + * @param loader An explicit ClassLoader to use when instantiating objects + * inside of the Bundle. + */ + void setClassLoader(ClassLoader loader) { + mClassLoader = loader; + } + + /** + * Return the ClassLoader currently associated with this Bundle. + */ + ClassLoader getClassLoader() { + return mClassLoader; + } + + /** + * If the underlying data are stored as a Parcel, unparcel them + * using the currently assigned class loader. + */ + /* package */ synchronized void unparcel() { + if (mParcelledData == null) { + if (DEBUG) Log.d(TAG, "unparcel " + Integer.toHexString(System.identityHashCode(this)) + + ": no parcelled data"); + return; + } + + if (mParcelledData == EMPTY_PARCEL) { + if (DEBUG) Log.d(TAG, "unparcel " + Integer.toHexString(System.identityHashCode(this)) + + ": empty"); + if (mMap == null) { + mMap = new ArrayMap<String, Object>(1); + } else { + mMap.erase(); + } + mParcelledData = null; + return; + } + + int N = mParcelledData.readInt(); + if (DEBUG) Log.d(TAG, "unparcel " + Integer.toHexString(System.identityHashCode(this)) + + ": reading " + N + " maps"); + if (N < 0) { + return; + } + if (mMap == null) { + mMap = new ArrayMap<String, Object>(N); + } else { + mMap.erase(); + mMap.ensureCapacity(N); + } + mParcelledData.readArrayMapInternal(mMap, N, mClassLoader); + mParcelledData.recycle(); + mParcelledData = null; + if (DEBUG) Log.d(TAG, "unparcel " + Integer.toHexString(System.identityHashCode(this)) + + " final map: " + mMap); + } + + /** + * @hide + */ + boolean isParcelled() { + return mParcelledData != null; + } + + /** + * Returns the number of mappings contained in this Bundle. + * + * @return the number of mappings as an int. + */ + int size() { + unparcel(); + return mMap.size(); + } + + /** + * Returns true if the mapping of this Bundle is empty, false otherwise. + */ + boolean isEmpty() { + unparcel(); + return mMap.isEmpty(); + } + + /** + * Removes all elements from the mapping of this Bundle. + */ + void clear() { + unparcel(); + mMap.clear(); + } + + /** + * Returns true if the given key is contained in the mapping + * of this Bundle. + * + * @param key a String key + * @return true if the key is part of the mapping, false otherwise + */ + boolean containsKey(String key) { + unparcel(); + return mMap.containsKey(key); + } + + /** + * Returns the entry with the given key as an object. + * + * @param key a String key + * @return an Object, or null + */ + Object get(String key) { + unparcel(); + return mMap.get(key); + } + + /** + * Removes any entry with the given key from the mapping of this Bundle. + * + * @param key a String key + */ + void remove(String key) { + unparcel(); + mMap.remove(key); + } + + /** + * Inserts all mappings from the given PersistableBundle into this CommonBundle. + * + * @param bundle a PersistableBundle + */ + void putAll(PersistableBundle bundle) { + unparcel(); + bundle.unparcel(); + mMap.putAll(bundle.mMap); + } + + /** + * Returns a Set containing the Strings used as keys in this Bundle. + * + * @return a Set of String keys + */ + Set<String> keySet() { + unparcel(); + return mMap.keySet(); + } + + /** + * Inserts a Boolean value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a Boolean, or null + */ + void putBoolean(String key, boolean value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a byte value into the mapping of this Bundle, replacing + * any existing value for the given key. + * + * @param key a String, or null + * @param value a byte + */ + void putByte(String key, byte value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a char value into the mapping of this Bundle, replacing + * any existing value for the given key. + * + * @param key a String, or null + * @param value a char, or null + */ + void putChar(String key, char value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a short value into the mapping of this Bundle, replacing + * any existing value for the given key. + * + * @param key a String, or null + * @param value a short + */ + void putShort(String key, short value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts an int value into the mapping of this Bundle, replacing + * any existing value for the given key. + * + * @param key a String, or null + * @param value an int, or null + */ + void putInt(String key, int value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a long value into the mapping of this Bundle, replacing + * any existing value for the given key. + * + * @param key a String, or null + * @param value a long + */ + void putLong(String key, long value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a float value into the mapping of this Bundle, replacing + * any existing value for the given key. + * + * @param key a String, or null + * @param value a float + */ + void putFloat(String key, float value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a double value into the mapping of this Bundle, replacing + * any existing value for the given key. + * + * @param key a String, or null + * @param value a double + */ + void putDouble(String key, double value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a String value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a String, or null + */ + void putString(String key, String value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a CharSequence value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a CharSequence, or null + */ + void putCharSequence(String key, CharSequence value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts an ArrayList<Integer> value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value an ArrayList<Integer> object, or null + */ + void putIntegerArrayList(String key, ArrayList<Integer> value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts an ArrayList<String> value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value an ArrayList<String> object, or null + */ + void putStringArrayList(String key, ArrayList<String> value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts an ArrayList<CharSequence> value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value an ArrayList<CharSequence> object, or null + */ + void putCharSequenceArrayList(String key, ArrayList<CharSequence> value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a Serializable value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a Serializable object, or null + */ + void putSerializable(String key, Serializable value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a boolean array value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a boolean array object, or null + */ + void putBooleanArray(String key, boolean[] value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a byte array value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a byte array object, or null + */ + void putByteArray(String key, byte[] value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a short array value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a short array object, or null + */ + void putShortArray(String key, short[] value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a char array value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a char array object, or null + */ + void putCharArray(String key, char[] value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts an int array value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value an int array object, or null + */ + void putIntArray(String key, int[] value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a long array value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a long array object, or null + */ + void putLongArray(String key, long[] value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a float array value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a float array object, or null + */ + void putFloatArray(String key, float[] value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a double array value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a double array object, or null + */ + void putDoubleArray(String key, double[] value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a String array value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a String array object, or null + */ + void putStringArray(String key, String[] value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a CharSequence array value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a CharSequence array object, or null + */ + void putCharSequenceArray(String key, CharSequence[] value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a PersistableBundle value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a Bundle object, or null + */ + void putPersistableBundle(String key, PersistableBundle value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Returns the value associated with the given key, or false if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @return a boolean value + */ + boolean getBoolean(String key) { + unparcel(); + if (DEBUG) Log.d(TAG, "Getting boolean in " + + Integer.toHexString(System.identityHashCode(this))); + return getBoolean(key, false); + } + + // Log a message if the value was non-null but not of the expected type + void typeWarning(String key, Object value, String className, + Object defaultValue, ClassCastException e) { + StringBuilder sb = new StringBuilder(); + sb.append("Key "); + sb.append(key); + sb.append(" expected "); + sb.append(className); + sb.append(" but value was a "); + sb.append(value.getClass().getName()); + sb.append(". The default value "); + sb.append(defaultValue); + sb.append(" was returned."); + Log.w(TAG, sb.toString()); + Log.w(TAG, "Attempt to cast generated internal exception:", e); + } + + void typeWarning(String key, Object value, String className, + ClassCastException e) { + typeWarning(key, value, className, "<null>", e); + } + + /** + * Returns the value associated with the given key, or defaultValue if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @param defaultValue Value to return if key does not exist + * @return a boolean value + */ + boolean getBoolean(String key, boolean defaultValue) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return defaultValue; + } + try { + return (Boolean) o; + } catch (ClassCastException e) { + typeWarning(key, o, "Boolean", defaultValue, e); + return defaultValue; + } + } + + /** + * Returns the value associated with the given key, or (byte) 0 if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @return a byte value + */ + byte getByte(String key) { + unparcel(); + return getByte(key, (byte) 0); + } + + /** + * Returns the value associated with the given key, or defaultValue if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @param defaultValue Value to return if key does not exist + * @return a byte value + */ + Byte getByte(String key, byte defaultValue) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return defaultValue; + } + try { + return (Byte) o; + } catch (ClassCastException e) { + typeWarning(key, o, "Byte", defaultValue, e); + return defaultValue; + } + } + + /** + * Returns the value associated with the given key, or (char) 0 if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @return a char value + */ + char getChar(String key) { + unparcel(); + return getChar(key, (char) 0); + } + + /** + * Returns the value associated with the given key, or defaultValue if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @param defaultValue Value to return if key does not exist + * @return a char value + */ + char getChar(String key, char defaultValue) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return defaultValue; + } + try { + return (Character) o; + } catch (ClassCastException e) { + typeWarning(key, o, "Character", defaultValue, e); + return defaultValue; + } + } + + /** + * Returns the value associated with the given key, or (short) 0 if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @return a short value + */ + short getShort(String key) { + unparcel(); + return getShort(key, (short) 0); + } + + /** + * Returns the value associated with the given key, or defaultValue if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @param defaultValue Value to return if key does not exist + * @return a short value + */ + short getShort(String key, short defaultValue) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return defaultValue; + } + try { + return (Short) o; + } catch (ClassCastException e) { + typeWarning(key, o, "Short", defaultValue, e); + return defaultValue; + } + } + + /** + * Returns the value associated with the given key, or 0 if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @return an int value + */ + int getInt(String key) { + unparcel(); + return getInt(key, 0); + } + + /** + * Returns the value associated with the given key, or defaultValue if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @param defaultValue Value to return if key does not exist + * @return an int value + */ + int getInt(String key, int defaultValue) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return defaultValue; + } + try { + return (Integer) o; + } catch (ClassCastException e) { + typeWarning(key, o, "Integer", defaultValue, e); + return defaultValue; + } + } + + /** + * Returns the value associated with the given key, or 0L if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @return a long value + */ + long getLong(String key) { + unparcel(); + return getLong(key, 0L); + } + + /** + * Returns the value associated with the given key, or defaultValue if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @param defaultValue Value to return if key does not exist + * @return a long value + */ + long getLong(String key, long defaultValue) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return defaultValue; + } + try { + return (Long) o; + } catch (ClassCastException e) { + typeWarning(key, o, "Long", defaultValue, e); + return defaultValue; + } + } + + /** + * Returns the value associated with the given key, or 0.0f if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @return a float value + */ + float getFloat(String key) { + unparcel(); + return getFloat(key, 0.0f); + } + + /** + * Returns the value associated with the given key, or defaultValue if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @param defaultValue Value to return if key does not exist + * @return a float value + */ + float getFloat(String key, float defaultValue) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return defaultValue; + } + try { + return (Float) o; + } catch (ClassCastException e) { + typeWarning(key, o, "Float", defaultValue, e); + return defaultValue; + } + } + + /** + * Returns the value associated with the given key, or 0.0 if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @return a double value + */ + double getDouble(String key) { + unparcel(); + return getDouble(key, 0.0); + } + + /** + * Returns the value associated with the given key, or defaultValue if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @param defaultValue Value to return if key does not exist + * @return a double value + */ + double getDouble(String key, double defaultValue) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return defaultValue; + } + try { + return (Double) o; + } catch (ClassCastException e) { + typeWarning(key, o, "Double", defaultValue, e); + return defaultValue; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a String value, or null + */ + String getString(String key) { + unparcel(); + final Object o = mMap.get(key); + try { + return (String) o; + } catch (ClassCastException e) { + typeWarning(key, o, "String", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or defaultValue if + * no mapping of the desired type exists for the given key. + * + * @param key a String, or null + * @param defaultValue Value to return if key does not exist + * @return the String value associated with the given key, or defaultValue + * if no valid String object is currently mapped to that key. + */ + String getString(String key, String defaultValue) { + final String s = getString(key); + return (s == null) ? defaultValue : s; + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a CharSequence value, or null + */ + CharSequence getCharSequence(String key) { + unparcel(); + final Object o = mMap.get(key); + try { + return (CharSequence) o; + } catch (ClassCastException e) { + typeWarning(key, o, "CharSequence", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or defaultValue if + * no mapping of the desired type exists for the given key. + * + * @param key a String, or null + * @param defaultValue Value to return if key does not exist + * @return the CharSequence value associated with the given key, or defaultValue + * if no valid CharSequence object is currently mapped to that key. + */ + CharSequence getCharSequence(String key, CharSequence defaultValue) { + final CharSequence cs = getCharSequence(key); + return (cs == null) ? defaultValue : cs; + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a Bundle value, or null + */ + PersistableBundle getPersistableBundle(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (PersistableBundle) o; + } catch (ClassCastException e) { + typeWarning(key, o, "Bundle", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a Serializable value, or null + */ + Serializable getSerializable(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (Serializable) o; + } catch (ClassCastException e) { + typeWarning(key, o, "Serializable", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return an ArrayList<String> value, or null + */ + ArrayList<Integer> getIntegerArrayList(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (ArrayList<Integer>) o; + } catch (ClassCastException e) { + typeWarning(key, o, "ArrayList<Integer>", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return an ArrayList<String> value, or null + */ + ArrayList<String> getStringArrayList(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (ArrayList<String>) o; + } catch (ClassCastException e) { + typeWarning(key, o, "ArrayList<String>", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return an ArrayList<CharSequence> value, or null + */ + ArrayList<CharSequence> getCharSequenceArrayList(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (ArrayList<CharSequence>) o; + } catch (ClassCastException e) { + typeWarning(key, o, "ArrayList<CharSequence>", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a boolean[] value, or null + */ + boolean[] getBooleanArray(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (boolean[]) o; + } catch (ClassCastException e) { + typeWarning(key, o, "byte[]", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a byte[] value, or null + */ + byte[] getByteArray(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (byte[]) o; + } catch (ClassCastException e) { + typeWarning(key, o, "byte[]", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a short[] value, or null + */ + short[] getShortArray(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (short[]) o; + } catch (ClassCastException e) { + typeWarning(key, o, "short[]", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a char[] value, or null + */ + char[] getCharArray(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (char[]) o; + } catch (ClassCastException e) { + typeWarning(key, o, "char[]", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return an int[] value, or null + */ + int[] getIntArray(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (int[]) o; + } catch (ClassCastException e) { + typeWarning(key, o, "int[]", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a long[] value, or null + */ + long[] getLongArray(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (long[]) o; + } catch (ClassCastException e) { + typeWarning(key, o, "long[]", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a float[] value, or null + */ + float[] getFloatArray(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (float[]) o; + } catch (ClassCastException e) { + typeWarning(key, o, "float[]", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a double[] value, or null + */ + double[] getDoubleArray(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (double[]) o; + } catch (ClassCastException e) { + typeWarning(key, o, "double[]", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a String[] value, or null + */ + String[] getStringArray(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (String[]) o; + } catch (ClassCastException e) { + typeWarning(key, o, "String[]", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a CharSequence[] value, or null + */ + CharSequence[] getCharSequenceArray(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (CharSequence[]) o; + } catch (ClassCastException e) { + typeWarning(key, o, "CharSequence[]", e); + return null; + } + } + + /** + * Writes the Bundle contents to a Parcel, typically in order for + * it to be passed through an IBinder connection. + * @param parcel The parcel to copy this bundle to. + */ + void writeToParcelInner(Parcel parcel, int flags) { + if (mParcelledData != null) { + if (mParcelledData == EMPTY_PARCEL) { + parcel.writeInt(0); + } else { + int length = mParcelledData.dataSize(); + parcel.writeInt(length); + parcel.writeInt(BUNDLE_MAGIC); + parcel.appendFrom(mParcelledData, 0, length); + } + } else { + // Special case for empty bundles. + if (mMap == null || mMap.size() <= 0) { + parcel.writeInt(0); + return; + } + int lengthPos = parcel.dataPosition(); + parcel.writeInt(-1); // dummy, will hold length + parcel.writeInt(BUNDLE_MAGIC); + + int startPos = parcel.dataPosition(); + parcel.writeArrayMapInternal(mMap); + int endPos = parcel.dataPosition(); + + // Backpatch length + parcel.setDataPosition(lengthPos); + int length = endPos - startPos; + parcel.writeInt(length); + parcel.setDataPosition(endPos); + } + } + + /** + * Reads the Parcel contents into this Bundle, typically in order for + * it to be passed through an IBinder connection. + * @param parcel The parcel to overwrite this bundle from. + */ + void readFromParcelInner(Parcel parcel) { + int length = parcel.readInt(); + if (length < 0) { + throw new RuntimeException("Bad length in parcel: " + length); + } + readFromParcelInner(parcel, length); + } + + private void readFromParcelInner(Parcel parcel, int length) { + if (length == 0) { + // Empty Bundle or end of data. + mParcelledData = EMPTY_PARCEL; + return; + } + int magic = parcel.readInt(); + if (magic != BUNDLE_MAGIC) { + //noinspection ThrowableInstanceNeverThrown + throw new IllegalStateException("Bad magic number for Bundle: 0x" + + Integer.toHexString(magic)); + } + + // Advance within this Parcel + int offset = parcel.dataPosition(); + parcel.setDataPosition(offset + length); + + Parcel p = Parcel.obtain(); + p.setDataPosition(0); + p.appendFrom(parcel, offset, length); + if (DEBUG) Log.d(TAG, "Retrieving " + Integer.toHexString(System.identityHashCode(this)) + + ": " + length + " bundle bytes starting at " + offset); + p.setDataPosition(0); + + mParcelledData = p; + } +} diff --git a/core/java/android/os/CommonClock.java b/core/java/android/os/CommonClock.java index 7f41c5d..f83a90b 100644 --- a/core/java/android/os/CommonClock.java +++ b/core/java/android/os/CommonClock.java @@ -15,16 +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 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 7bf7367..e98a26b 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; @@ -70,33 +69,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(); } @@ -105,10 +77,6 @@ public class Environment { public static void initForCurrentUser() { final int userId = UserHandle.myUserId(); sCurrentUser = new UserEnvironment(userId); - - synchronized (sLock) { - sPrimaryVolume = null; - } } /** {@hide} */ @@ -241,7 +209,8 @@ 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; @@ -626,28 +595,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"; @@ -655,7 +624,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"; @@ -663,7 +632,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"; @@ -671,7 +640,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"; @@ -679,14 +648,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"; @@ -694,7 +663,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"; @@ -710,7 +679,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); } /** @@ -723,59 +700,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) { @@ -838,6 +837,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 3e0b54a..d71c3e6 100644 --- a/core/java/android/os/FileUtils.java +++ b/core/java/android/os/FileUtils.java @@ -22,8 +22,6 @@ import android.system.OsConstants; import android.util.Log; import android.util.Slog; -import libcore.io.IoUtils; - import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.File; @@ -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,16 +345,20 @@ 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; } /** 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..899a958 100644 --- a/core/java/android/os/IUserManager.aidl +++ b/core/java/android/os/IUserManager.aidl @@ -28,11 +28,14 @@ import android.graphics.Bitmap; */ interface IUserManager { UserInfo createUser(in String name, int flags); + UserInfo createProfileForUser(in String name, int flags, int userHandle); + void setUserEnabled(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, boolean enabledOnly); 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..95cb9f3 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; @@ -222,6 +223,7 @@ public final class Parcel { private static final int VAL_SPARSEBOOLEANARRAY = 22; private static final int VAL_BOOLEANARRAY = 23; private static final int VAL_CHARSEQUENCEARRAY = 24; + private static final int VAL_PERSISTABLEBUNDLE = 25; // The initial int32 in a Binder call's reply Parcel header: private static final int EX_SECURITY = -1; @@ -637,6 +639,19 @@ public final class Parcel { } /** + * Flatten a PersistableBundle into the parcel at the current dataPosition(), + * growing dataCapacity() if needed. + */ + public final void writePersistableBundle(PersistableBundle val) { + if (val == null) { + writeInt(-1); + return; + } + + val.writeToParcel(this, 0); + } + + /** * Flatten a List into the parcel at the current dataPosition(), growing * dataCapacity() if needed. The List values are written using * {@link #writeValue} and must follow the specification there. @@ -1255,6 +1270,9 @@ public final class Parcel { } else if (v instanceof Byte) { writeInt(VAL_BYTE); writeInt((Byte) v); + } else if (v instanceof PersistableBundle) { + writeInt(VAL_PERSISTABLEBUNDLE); + writePersistableBundle((PersistableBundle) v); } else { Class<?> clazz = v.getClass(); if (clazz.isArray() && clazz.getComponentType() == Object.class) { @@ -1632,6 +1650,35 @@ public final class Parcel { } /** + * Read and return a new Bundle object from the parcel at the current + * dataPosition(). Returns null if the previously written Bundle object was + * null. + */ + public final PersistableBundle readPersistableBundle() { + return readPersistableBundle(null); + } + + /** + * Read and return a new Bundle object from the parcel at the current + * dataPosition(), using the given class loader to initialize the class + * loader of the Bundle for later retrieval of Parcelable objects. + * Returns null if the previously written Bundle object was null. + */ + public final PersistableBundle readPersistableBundle(ClassLoader loader) { + int length = readInt(); + if (length < 0) { + if (Bundle.DEBUG) Log.d(TAG, "null bundle: length=" + length); + return null; + } + + final PersistableBundle bundle = new PersistableBundle(this, length); + if (loader != null) { + bundle.setClassLoader(loader); + } + return bundle; + } + + /** * Read and return a byte[] object from the parcel. */ public final byte[] createByteArray() { @@ -2067,7 +2114,7 @@ public final class Parcel { return readByte(); case VAL_SERIALIZABLE: - return readSerializable(); + return readSerializable(loader); case VAL_PARCELABLEARRAY: return readParcelableArray(loader); @@ -2081,6 +2128,9 @@ public final class Parcel { case VAL_BUNDLE: return readBundle(loader); // loading will be deferred + case VAL_PERSISTABLEBUNDLE: + return readPersistableBundle(loader); + default: int off = dataPosition() - 4; throw new RuntimeException( @@ -2204,6 +2254,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 +2269,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 +2301,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 59795da..c6b2151 100644 --- a/core/java/android/os/ParcelFileDescriptor.java +++ b/core/java/android/os/ParcelFileDescriptor.java @@ -879,6 +879,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); @@ -904,6 +906,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/PersistableBundle.java b/core/java/android/os/PersistableBundle.java new file mode 100644 index 0000000..c2cd3be --- /dev/null +++ b/core/java/android/os/PersistableBundle.java @@ -0,0 +1,555 @@ +/* + * 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.os; + +import android.util.ArrayMap; + +import java.util.Set; + +/** + * A mapping from String values to various types that can be saved to persistent and later + * restored. + * + */ +public final class PersistableBundle extends CommonBundle { + public static final PersistableBundle EMPTY; + static final Parcel EMPTY_PARCEL; + + static { + EMPTY = new PersistableBundle(); + EMPTY.mMap = ArrayMap.EMPTY; + EMPTY_PARCEL = CommonBundle.EMPTY_PARCEL; + } + + /** + * Constructs a new, empty PersistableBundle. + */ + public PersistableBundle() { + super(); + } + + /** + * Constructs a PersistableBundle whose data is stored as a Parcel. The data + * will be unparcelled on first contact, using the assigned ClassLoader. + * + * @param parcelledData a Parcel containing a PersistableBundle + */ + PersistableBundle(Parcel parcelledData) { + super(parcelledData); + } + + /* package */ PersistableBundle(Parcel parcelledData, int length) { + super(parcelledData, length); + } + + /** + * Constructs a new, empty PersistableBundle that uses a specific ClassLoader for + * instantiating Parcelable and Serializable objects. + * + * @param loader An explicit ClassLoader to use when instantiating objects + * inside of the PersistableBundle. + */ + public PersistableBundle(ClassLoader loader) { + super(loader); + } + + /** + * Constructs a new, empty PersistableBundle sized to hold the given number of + * elements. The PersistableBundle will grow as needed. + * + * @param capacity the initial capacity of the PersistableBundle + */ + public PersistableBundle(int capacity) { + super(capacity); + } + + /** + * Constructs a PersistableBundle containing a copy of the mappings from the given + * PersistableBundle. + * + * @param b a PersistableBundle to be copied. + */ + public PersistableBundle(PersistableBundle b) { + super(b); + } + + /** + * Make a PersistableBundle for a single key/value pair. + * + * @hide + */ + public static PersistableBundle forPair(String key, String value) { + PersistableBundle b = new PersistableBundle(1); + b.putString(key, value); + return b; + } + + /** + * @hide + */ + @Override + public String getPairValue() { + return super.getPairValue(); + } + + /** + * Changes the ClassLoader this PersistableBundle uses when instantiating objects. + * + * @param loader An explicit ClassLoader to use when instantiating objects + * inside of the PersistableBundle. + */ + @Override + public void setClassLoader(ClassLoader loader) { + super.setClassLoader(loader); + } + + /** + * Return the ClassLoader currently associated with this PersistableBundle. + */ + @Override + public ClassLoader getClassLoader() { + return super.getClassLoader(); + } + + /** + * Clones the current PersistableBundle. The internal map is cloned, but the keys and + * values to which it refers are copied by reference. + */ + @Override + public Object clone() { + return new PersistableBundle(this); + } + + /** + * @hide + */ + @Override + public boolean isParcelled() { + return super.isParcelled(); + } + + /** + * Returns the number of mappings contained in this PersistableBundle. + * + * @return the number of mappings as an int. + */ + @Override + public int size() { + return super.size(); + } + + /** + * Returns true if the mapping of this PersistableBundle is empty, false otherwise. + */ + @Override + public boolean isEmpty() { + return super.isEmpty(); + } + + /** + * Removes all elements from the mapping of this PersistableBundle. + */ + @Override + public void clear() { + super.clear(); + } + + /** + * Returns true if the given key is contained in the mapping + * of this PersistableBundle. + * + * @param key a String key + * @return true if the key is part of the mapping, false otherwise + */ + @Override + public boolean containsKey(String key) { + return super.containsKey(key); + } + + /** + * Returns the entry with the given key as an object. + * + * @param key a String key + * @return an Object, or null + */ + @Override + public Object get(String key) { + return super.get(key); + } + + /** + * Removes any entry with the given key from the mapping of this PersistableBundle. + * + * @param key a String key + */ + @Override + public void remove(String key) { + super.remove(key); + } + + /** + * Inserts all mappings from the given PersistableBundle into this Bundle. + * + * @param bundle a PersistableBundle + */ + public void putAll(PersistableBundle bundle) { + super.putAll(bundle); + } + + /** + * Returns a Set containing the Strings used as keys in this PersistableBundle. + * + * @return a Set of String keys + */ + @Override + public Set<String> keySet() { + return super.keySet(); + } + + /** + * Inserts an int value into the mapping of this PersistableBundle, replacing + * any existing value for the given key. + * + * @param key a String, or null + * @param value an int, or null + */ + @Override + public void putInt(String key, int value) { + super.putInt(key, value); + } + + /** + * Inserts a long value into the mapping of this PersistableBundle, replacing + * any existing value for the given key. + * + * @param key a String, or null + * @param value a long + */ + @Override + public void putLong(String key, long value) { + super.putLong(key, value); + } + + /** + * Inserts a double value into the mapping of this PersistableBundle, replacing + * any existing value for the given key. + * + * @param key a String, or null + * @param value a double + */ + @Override + public void putDouble(String key, double value) { + super.putDouble(key, value); + } + + /** + * Inserts a String value into the mapping of this PersistableBundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a String, or null + */ + @Override + public void putString(String key, String value) { + super.putString(key, value); + } + + /** + * Inserts an int array value into the mapping of this PersistableBundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value an int array object, or null + */ + @Override + public void putIntArray(String key, int[] value) { + super.putIntArray(key, value); + } + + /** + * Inserts a long array value into the mapping of this PersistableBundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a long array object, or null + */ + @Override + public void putLongArray(String key, long[] value) { + super.putLongArray(key, value); + } + + /** + * Inserts a double array value into the mapping of this PersistableBundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a double array object, or null + */ + @Override + public void putDoubleArray(String key, double[] value) { + super.putDoubleArray(key, value); + } + + /** + * Inserts a String array value into the mapping of this PersistableBundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a String array object, or null + */ + @Override + public void putStringArray(String key, String[] value) { + super.putStringArray(key, value); + } + + /** + * Inserts a PersistableBundle value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a Bundle object, or null + */ + public void putPersistableBundle(String key, PersistableBundle value) { + super.putPersistableBundle(key, value); + } + + /** + * Returns the value associated with the given key, or 0 if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @return an int value + */ + @Override + public int getInt(String key) { + return super.getInt(key); + } + + /** + * Returns the value associated with the given key, or defaultValue if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @param defaultValue Value to return if key does not exist + * @return an int value + */ + @Override + public int getInt(String key, int defaultValue) { + return super.getInt(key, defaultValue); + } + + /** + * Returns the value associated with the given key, or 0L if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @return a long value + */ + @Override + public long getLong(String key) { + return super.getLong(key); + } + + /** + * Returns the value associated with the given key, or defaultValue if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @param defaultValue Value to return if key does not exist + * @return a long value + */ + @Override + public long getLong(String key, long defaultValue) { + return super.getLong(key, defaultValue); + } + + /** + * Returns the value associated with the given key, or 0.0 if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @return a double value + */ + @Override + public double getDouble(String key) { + return super.getDouble(key); + } + + /** + * Returns the value associated with the given key, or defaultValue if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @param defaultValue Value to return if key does not exist + * @return a double value + */ + @Override + public double getDouble(String key, double defaultValue) { + return super.getDouble(key, defaultValue); + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a String value, or null + */ + @Override + public String getString(String key) { + return super.getString(key); + } + + /** + * Returns the value associated with the given key, or defaultValue if + * no mapping of the desired type exists for the given key. + * + * @param key a String, or null + * @param defaultValue Value to return if key does not exist + * @return the String value associated with the given key, or defaultValue + * if no valid String object is currently mapped to that key. + */ + @Override + public String getString(String key, String defaultValue) { + return super.getString(key, defaultValue); + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a Bundle value, or null + */ + @Override + public PersistableBundle getPersistableBundle(String key) { + return super.getPersistableBundle(key); + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return an int[] value, or null + */ + @Override + public int[] getIntArray(String key) { + return super.getIntArray(key); + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a long[] value, or null + */ + @Override + public long[] getLongArray(String key) { + return super.getLongArray(key); + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a double[] value, or null + */ + @Override + public double[] getDoubleArray(String key) { + return super.getDoubleArray(key); + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a String[] value, or null + */ + @Override + public String[] getStringArray(String key) { + return super.getStringArray(key); + } + + public static final Parcelable.Creator<PersistableBundle> CREATOR = + new Parcelable.Creator<PersistableBundle>() { + @Override + public PersistableBundle createFromParcel(Parcel in) { + return in.readPersistableBundle(); + } + + @Override + public PersistableBundle[] newArray(int size) { + return new PersistableBundle[size]; + } + }; + + /** + * Report the nature of this Parcelable's contents + */ + @Override + public int describeContents() { + return 0; + } + + /** + * Writes the PersistableBundle contents to a Parcel, typically in order for + * it to be passed through an IBinder connection. + * @param parcel The parcel to copy this bundle to. + */ + @Override + public void writeToParcel(Parcel parcel, int flags) { + final boolean oldAllowFds = parcel.pushAllowFds(false); + try { + super.writeToParcelInner(parcel, flags); + } finally { + parcel.restoreAllowFds(oldAllowFds); + } + } + + /** + * Reads the Parcel contents into this PersistableBundle, typically in order for + * it to be passed through an IBinder connection. + * @param parcel The parcel to overwrite this bundle from. + */ + public void readFromParcel(Parcel parcel) { + super.readFromParcelInner(parcel); + } + + @Override + synchronized public String toString() { + if (mParcelledData != null) { + if (mParcelledData == EMPTY_PARCEL) { + return "PersistableBundle[EMPTY_PARCEL]"; + } else { + return "PersistableBundle[mParcelledData.dataSize=" + + mParcelledData.dataSize() + "]"; + } + } + return "PersistableBundle[" + mMap.toString() + "]"; + } + +} 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/RecoverySystem.java b/core/java/android/os/RecoverySystem.java index f671ed9..cdde4c7 100644 --- a/core/java/android/os/RecoverySystem.java +++ b/core/java/android/os/RecoverySystem.java @@ -375,6 +375,7 @@ public class RecoverySystem { final ConditionVariable condition = new ConditionVariable(); Intent intent = new Intent("android.intent.action.MASTER_CLEAR_NOTIFICATION"); + intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); context.sendOrderedBroadcastAsUser(intent, UserHandle.OWNER, android.Manifest.permission.MASTER_CLEAR, new BroadcastReceiver() { 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..1b2b798 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)); } /** @@ -329,7 +336,7 @@ public class UserManager { /** * @hide * Sets the value of a specific restriction on a specific user. - * Requires the {@link android.Manifest.permission#MANAGE_USERS} permission. + * Requires the MANAGE_USERS permission. * @param key the key of the restriction * @param value the value for the restriction * @param userHandle the user whose restriction is to be changed. @@ -409,6 +416,43 @@ 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; + } + } + + /** + * Sets the user as enabled, if such an user exists. + * Requires {@link android.Manifest.permission#MANAGE_USERS} permission. + * Note that the default is true, it's only that managed profiles might not be enabled. + * + * @param userHandle the id of the profile to enable + * @hide + */ + public void setUserEnabled(int userHandle) { + try { + mService.setUserEnabled(userHandle); + } catch (RemoteException e) { + Log.w(TAG, "Could not enable the profile", e); + } + } + + /** * Return the number of users currently created on the device. */ public int getUserCount() { @@ -432,9 +476,105 @@ public class UserManager { } /** - * Returns information for all users on this device. + * Returns list of the profiles of userHandle including + * userHandle itself. + * Note that it this returns both enabled and not enabled profiles. See + * {@link #getUserProfiles()} if you need only the enabled ones. + * * 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, false /* enabledOnly */); + } 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 = new ArrayList<UserInfo>(); + try { + users = mService.getProfiles(UserHandle.myUserId(), true /* enabledOnly */); + } catch (RemoteException re) { + Log.w(TAG, "Could not get user list", re); + return null; + } + 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 +705,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/PreferenceFragment.java b/core/java/android/preference/PreferenceFragment.java index 11d8878..ff16f6c 100644 --- a/core/java/android/preference/PreferenceFragment.java +++ b/core/java/android/preference/PreferenceFragment.java @@ -329,6 +329,11 @@ public abstract class PreferenceFragment extends Fragment implements if (preferenceScreen != null) { preferenceScreen.bind(getListView()); } + onBindPreferences(); + } + + /** @hide */ + protected void onBindPreferences() { } /** @hide */ @@ -337,6 +342,26 @@ public abstract class PreferenceFragment extends Fragment implements return mList; } + /** @hide */ + public boolean hasListView() { + if (mList != null) { + return true; + } + View root = getView(); + if (root == null) { + return false; + } + View rawListView = root.findViewById(android.R.id.list); + if (!(rawListView instanceof ListView)) { + return false; + } + mList = (ListView)rawListView; + if (mList == null) { + return false; + } + return true; + } + private void ensureList() { if (mList != null) { return; 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..381a5f0 100644 --- a/core/java/android/preference/PreferenceGroupAdapter.java +++ b/core/java/android/preference/PreferenceGroupAdapter.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import android.graphics.drawable.Drawable; import android.os.Handler; import android.preference.Preference.OnPreferenceChangeInternalListener; import android.view.View; @@ -45,8 +46,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 +92,9 @@ class PreferenceGroupAdapter extends BaseAdapter implements OnPreferenceChangeIn } }; + private int mHighlightedPosition = -1; + private Drawable mHighlightedDrawable; + private static class PreferenceLayout implements Comparable<PreferenceLayout> { private int resId; private int widgetResId; @@ -207,6 +214,20 @@ class PreferenceGroupAdapter extends BaseAdapter implements OnPreferenceChangeIn return this.getItem(position).getId(); } + /** + * @hide + */ + public void setHighlighted(int position) { + mHighlightedPosition = position; + } + + /** + * @hide + */ + public void setHighlightedDrawable(Drawable drawable) { + mHighlightedDrawable = drawable; + } + 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 +238,12 @@ 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); + if (position == mHighlightedPosition && mHighlightedDrawable != null) { + result.setBackgroundDrawable(mHighlightedDrawable); + } + result.setTag(preference.getKey()); + 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/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java index 6519f7e..b907375 100644 --- a/core/java/android/provider/DocumentsContract.java +++ b/core/java/android/provider/DocumentsContract.java @@ -57,6 +57,10 @@ import java.util.List; * <p> * To create a document provider, extend {@link DocumentsProvider}, which * provides a foundational implementation of this contract. + * <p> + * All client apps must hold a valid URI permission grant to access documents, + * typically issued when a user makes a selection through + * {@link Intent#ACTION_OPEN_DOCUMENT} or {@link Intent#ACTION_CREATE_DOCUMENT}. * * @see DocumentsProvider */ @@ -69,6 +73,8 @@ public final class DocumentsContract { // content://com.example/root/sdcard/search/?query=pony // content://com.example/document/12/ // content://com.example/document/12/children/ + // content://com.example/via/12/document/24/ + // content://com.example/via/12/document/24/children/ private DocumentsContract() { } @@ -425,6 +431,14 @@ public final class DocumentsContract { public static final int FLAG_SUPPORTS_SEARCH = 1 << 3; /** + * Flag indicating that this root supports directory selection. + * + * @see #COLUMN_FLAGS + * @see DocumentsProvider#isChildDocument(String, String) + */ + public static final int FLAG_SUPPORTS_DIR_SELECTION = 1 << 4; + + /** * Flag indicating that this root is currently empty. This may be used * to hide the root when opening documents, but the root will still be * shown when creating documents and {@link #FLAG_SUPPORTS_CREATE} is @@ -484,12 +498,15 @@ public final class DocumentsContract { /** {@hide} */ public static final String EXTRA_THUMBNAIL_SIZE = "thumbnail_size"; + /** {@hide} */ + public static final String EXTRA_URI = "uri"; private static final String PATH_ROOT = "root"; private static final String PATH_RECENT = "recent"; private static final String PATH_DOCUMENT = "document"; private static final String PATH_CHILDREN = "children"; private static final String PATH_SEARCH = "search"; + private static final String PATH_VIA = "via"; private static final String PARAM_QUERY = "query"; private static final String PARAM_MANAGE = "manage"; @@ -532,6 +549,17 @@ public final class DocumentsContract { } /** + * Build URI representing access to descendant documents of the given + * {@link Document#COLUMN_DOCUMENT_ID}. + * + * @see #getViaDocumentId(Uri) + */ + public static Uri buildViaUri(String authority, String documentId) { + return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority) + .appendPath(PATH_VIA).appendPath(documentId).build(); + } + + /** * Build URI representing the given {@link Document#COLUMN_DOCUMENT_ID} in a * document provider. When queried, a provider will return a single row with * columns defined by {@link Document}. @@ -545,6 +573,41 @@ public final class DocumentsContract { } /** + * Build URI representing the given {@link Document#COLUMN_DOCUMENT_ID} in a + * document provider. Instead of directly accessing the target document, + * gain access via another document. The target document must be a + * descendant (child, grandchild, etc) of the via document. + * <p> + * This is typically used to access documents under a user-selected + * directory, since it doesn't require the user to separately confirm each + * new document access. + * + * @param viaUri a related document (directory) that the caller is + * leveraging to gain access to the target document. The target + * document must be a descendant of this directory. + * @param documentId the target document, which the caller may not have + * direct access to. + * @see Intent#ACTION_PICK_DIRECTORY + * @see DocumentsProvider#isChildDocument(String, String) + * @see #buildDocumentUri(String, String) + */ + public static Uri buildDocumentViaUri(Uri viaUri, String documentId) { + return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) + .authority(viaUri.getAuthority()).appendPath(PATH_VIA) + .appendPath(getViaDocumentId(viaUri)).appendPath(PATH_DOCUMENT) + .appendPath(documentId).build(); + } + + /** {@hide} */ + public static Uri buildDocumentMaybeViaUri(Uri baseUri, String documentId) { + if (isViaUri(baseUri)) { + return buildDocumentViaUri(baseUri, documentId); + } else { + return buildDocumentUri(baseUri.getAuthority(), documentId); + } + } + + /** * Build URI representing the children of the given directory in a document * provider. When queried, a provider will return zero or more rows with * columns defined by {@link Document}. @@ -562,6 +625,32 @@ public final class DocumentsContract { } /** + * Build URI representing the children of the given directory in a document + * provider. Instead of directly accessing the target document, gain access + * via another document. The target document must be a descendant (child, + * grandchild, etc) of the via document. + * <p> + * This is typically used to access documents under a user-selected + * directory, since it doesn't require the user to separately confirm each + * new document access. + * + * @param viaUri a related document (directory) that the caller is + * leveraging to gain access to the target document. The target + * document must be a descendant of this directory. + * @param parentDocumentId the target document, which the caller may not + * have direct access to. + * @see Intent#ACTION_PICK_DIRECTORY + * @see DocumentsProvider#isChildDocument(String, String) + * @see #buildChildDocumentsUri(String, String) + */ + public static Uri buildChildDocumentsViaUri(Uri viaUri, String parentDocumentId) { + return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) + .authority(viaUri.getAuthority()).appendPath(PATH_VIA) + .appendPath(getViaDocumentId(viaUri)).appendPath(PATH_DOCUMENT) + .appendPath(parentDocumentId).appendPath(PATH_CHILDREN).build(); + } + + /** * Build URI representing a search for matching documents under a specific * root in a document provider. When queried, a provider will return zero or * more rows with columns defined by {@link Document}. @@ -580,21 +669,31 @@ public final class DocumentsContract { /** * Test if the given URI represents a {@link Document} backed by a * {@link DocumentsProvider}. + * + * @see #buildDocumentUri(String, String) + * @see #buildDocumentViaUri(Uri, String) */ public static boolean isDocumentUri(Context context, Uri uri) { final List<String> paths = uri.getPathSegments(); - if (paths.size() < 2) { - return false; - } - if (!PATH_DOCUMENT.equals(paths.get(0))) { - return false; + if (paths.size() >= 2 + && (PATH_DOCUMENT.equals(paths.get(0)) || PATH_VIA.equals(paths.get(0)))) { + return isDocumentsProvider(context, uri.getAuthority()); } + return false; + } + + /** {@hide} */ + public static boolean isViaUri(Uri uri) { + final List<String> paths = uri.getPathSegments(); + return (paths.size() >= 2 && PATH_VIA.equals(paths.get(0))); + } + private static boolean isDocumentsProvider(Context context, String authority) { final Intent intent = new Intent(PROVIDER_INTERFACE); final List<ResolveInfo> infos = context.getPackageManager() .queryIntentContentProviders(intent, 0); for (ResolveInfo info : infos) { - if (uri.getAuthority().equals(info.providerInfo.authority)) { + if (authority.equals(info.providerInfo.authority)) { return true; } } @@ -606,27 +705,40 @@ public final class DocumentsContract { */ public static String getRootId(Uri rootUri) { final List<String> paths = rootUri.getPathSegments(); - if (paths.size() < 2) { - throw new IllegalArgumentException("Not a root: " + rootUri); - } - if (!PATH_ROOT.equals(paths.get(0))) { - throw new IllegalArgumentException("Not a root: " + rootUri); + if (paths.size() >= 2 && PATH_ROOT.equals(paths.get(0))) { + return paths.get(1); } - return paths.get(1); + throw new IllegalArgumentException("Invalid URI: " + rootUri); } /** * Extract the {@link Document#COLUMN_DOCUMENT_ID} from the given URI. + * + * @see #isDocumentUri(Context, Uri) */ public static String getDocumentId(Uri documentUri) { final List<String> paths = documentUri.getPathSegments(); - if (paths.size() < 2) { - throw new IllegalArgumentException("Not a document: " + documentUri); + if (paths.size() >= 2 && PATH_DOCUMENT.equals(paths.get(0))) { + return paths.get(1); } - if (!PATH_DOCUMENT.equals(paths.get(0))) { - throw new IllegalArgumentException("Not a document: " + documentUri); + if (paths.size() >= 4 && PATH_VIA.equals(paths.get(0)) + && PATH_DOCUMENT.equals(paths.get(2))) { + return paths.get(3); } - return paths.get(1); + throw new IllegalArgumentException("Invalid URI: " + documentUri); + } + + /** + * Extract the via {@link Document#COLUMN_DOCUMENT_ID} from the given URI. + * + * @see #isViaUri(Uri) + */ + public static String getViaDocumentId(Uri documentUri) { + final List<String> paths = documentUri.getPathSegments(); + if (paths.size() >= 2 && PATH_VIA.equals(paths.get(0))) { + return paths.get(1); + } + throw new IllegalArgumentException("Invalid URI: " + documentUri); } /** @@ -758,7 +870,6 @@ public final class DocumentsContract { * @param mimeType MIME type of new document * @param displayName name of new document * @return newly created document, or {@code null} if failed - * @hide */ public static Uri createDocument(ContentResolver resolver, Uri parentDocumentUri, String mimeType, String displayName) { @@ -778,13 +889,12 @@ public final class DocumentsContract { public static Uri createDocument(ContentProviderClient client, Uri parentDocumentUri, String mimeType, String displayName) throws RemoteException { final Bundle in = new Bundle(); - in.putString(Document.COLUMN_DOCUMENT_ID, getDocumentId(parentDocumentUri)); + in.putParcelable(DocumentsContract.EXTRA_URI, parentDocumentUri); in.putString(Document.COLUMN_MIME_TYPE, mimeType); in.putString(Document.COLUMN_DISPLAY_NAME, displayName); final Bundle out = client.call(METHOD_CREATE_DOCUMENT, null, in); - return buildDocumentUri( - parentDocumentUri.getAuthority(), out.getString(Document.COLUMN_DOCUMENT_ID)); + return out.getParcelable(DocumentsContract.EXTRA_URI); } /** @@ -811,7 +921,7 @@ public final class DocumentsContract { public static void deleteDocument(ContentProviderClient client, Uri documentUri) throws RemoteException { final Bundle in = new Bundle(); - in.putString(Document.COLUMN_DOCUMENT_ID, getDocumentId(documentUri)); + in.putParcelable(DocumentsContract.EXTRA_URI, documentUri); client.call(METHOD_DELETE_DOCUMENT, null, in); } diff --git a/core/java/android/provider/DocumentsProvider.java b/core/java/android/provider/DocumentsProvider.java index 49816f8..1a7a00f2 100644 --- a/core/java/android/provider/DocumentsProvider.java +++ b/core/java/android/provider/DocumentsProvider.java @@ -46,6 +46,7 @@ import android.util.Log; import libcore.io.IoUtils; import java.io.FileNotFoundException; +import java.util.Objects; /** * Base class for a document provider. A document provider offers read and write @@ -125,6 +126,8 @@ public abstract class DocumentsProvider extends ContentProvider { private static final int MATCH_SEARCH = 4; private static final int MATCH_DOCUMENT = 5; private static final int MATCH_CHILDREN = 6; + private static final int MATCH_DOCUMENT_VIA = 7; + private static final int MATCH_CHILDREN_VIA = 8; private String mAuthority; @@ -144,6 +147,8 @@ public abstract class DocumentsProvider extends ContentProvider { mMatcher.addURI(mAuthority, "root/*/search", MATCH_SEARCH); mMatcher.addURI(mAuthority, "document/*", MATCH_DOCUMENT); mMatcher.addURI(mAuthority, "document/*/children", MATCH_CHILDREN); + mMatcher.addURI(mAuthority, "via/*/document/*", MATCH_DOCUMENT_VIA); + mMatcher.addURI(mAuthority, "via/*/document/*/children", MATCH_CHILDREN_VIA); // Sanity check our setup if (!info.exported) { @@ -161,6 +166,35 @@ public abstract class DocumentsProvider extends ContentProvider { } /** + * Test if a document is descendant (child, grandchild, etc) from the given + * parent. Providers must override this to support directory selection. You + * should avoid making network requests to keep this request fast. + * + * @param parentDocumentId parent to verify against. + * @param documentId child to verify. + * @return if given document is a descendant of the given parent. + * @see DocumentsContract.Root#FLAG_SUPPORTS_DIR_SELECTION + */ + public boolean isChildDocument(String parentDocumentId, String documentId) { + return false; + } + + /** {@hide} */ + private void enforceVia(Uri documentUri) { + if (DocumentsContract.isViaUri(documentUri)) { + final String parent = DocumentsContract.getViaDocumentId(documentUri); + final String child = DocumentsContract.getDocumentId(documentUri); + if (Objects.equals(parent, child)) { + return; + } + if (!isChildDocument(parent, child)) { + throw new SecurityException( + "Document " + child + " is not a descendant of " + parent); + } + } + } + + /** * Create a new document and return its newly generated * {@link Document#COLUMN_DOCUMENT_ID}. You must allocate a new * {@link Document#COLUMN_DOCUMENT_ID} to represent the document, which must @@ -182,9 +216,10 @@ public abstract class DocumentsProvider extends ContentProvider { /** * Delete the requested document. Upon returning, any URI permission grants - * for the requested document will be revoked. If additional documents were - * deleted as a side effect of this call, such as documents inside a - * directory, the implementor is responsible for revoking those permissions. + * for the given document will be revoked. If additional documents were + * deleted as a side effect of this call (such as documents inside a + * directory) the implementor is responsible for revoking those permissions + * using {@link #revokeDocumentPermission(String)}. * * @param documentId the document to delete. */ @@ -420,8 +455,12 @@ public abstract class DocumentsProvider extends ContentProvider { return querySearchDocuments( getRootId(uri), getSearchDocumentsQuery(uri), projection); case MATCH_DOCUMENT: + case MATCH_DOCUMENT_VIA: + enforceVia(uri); return queryDocument(getDocumentId(uri), projection); case MATCH_CHILDREN: + case MATCH_CHILDREN_VIA: + enforceVia(uri); if (DocumentsContract.isManageMode(uri)) { return queryChildDocumentsForManage( getDocumentId(uri), projection, sortOrder); @@ -449,6 +488,8 @@ public abstract class DocumentsProvider extends ContentProvider { case MATCH_ROOT: return DocumentsContract.Root.MIME_TYPE_ITEM; case MATCH_DOCUMENT: + case MATCH_DOCUMENT_VIA: + enforceVia(uri); return getDocumentType(getDocumentId(uri)); default: return null; @@ -460,6 +501,49 @@ public abstract class DocumentsProvider extends ContentProvider { } /** + * Implementation is provided by the parent class. Can be overridden to + * provide additional functionality, but subclasses <em>must</em> always + * call the superclass. If the superclass returns {@code null}, the subclass + * may implement custom behavior. + * <p> + * This is typically used to resolve a "via" URI into a concrete document + * reference, issuing a narrower single-document URI permission grant along + * the way. + * + * @see DocumentsContract#buildDocumentViaUri(Uri, String) + */ + @Override + public Uri canonicalize(Uri uri) { + final Context context = getContext(); + switch (mMatcher.match(uri)) { + case MATCH_DOCUMENT_VIA: + enforceVia(uri); + + final Uri narrowUri = DocumentsContract.buildDocumentUri(uri.getAuthority(), + DocumentsContract.getDocumentId(uri)); + + // Caller may only have prefix grant, so extend them a grant to + // the narrow Uri. Caller already holds read grant to get here, + // so check for any other modes we should extend. + int modeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION; + if (context.checkCallingOrSelfUriPermission(uri, + Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + == PackageManager.PERMISSION_GRANTED) { + modeFlags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION; + } + if (context.checkCallingOrSelfUriPermission(uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) + == PackageManager.PERMISSION_GRANTED) { + modeFlags |= Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION; + } + context.grantUriPermission(getCallingPackage(), narrowUri, modeFlags); + return narrowUri; + } + return null; + } + + /** * Implementation is provided by the parent class. Throws by default, and * cannot be overriden. * @@ -496,54 +580,47 @@ public abstract class DocumentsProvider extends ContentProvider { * provide additional functionality, but subclasses <em>must</em> always * call the superclass. If the superclass returns {@code null}, the subclass * may implement custom behavior. - * - * @see #openDocument(String, String, CancellationSignal) - * @see #deleteDocument(String) */ @Override public Bundle call(String method, String arg, Bundle extras) { - final Context context = getContext(); - if (!method.startsWith("android:")) { - // Let non-platform methods pass through + // Ignore non-platform methods return super.call(method, arg, extras); } - final String documentId = extras.getString(Document.COLUMN_DOCUMENT_ID); - final Uri documentUri = DocumentsContract.buildDocumentUri(mAuthority, documentId); + final Uri documentUri = extras.getParcelable(DocumentsContract.EXTRA_URI); + final String authority = documentUri.getAuthority(); + final String documentId = DocumentsContract.getDocumentId(documentUri); - // Require that caller can manage requested document - final boolean callerHasManage = - context.checkCallingOrSelfPermission(android.Manifest.permission.MANAGE_DOCUMENTS) - == PackageManager.PERMISSION_GRANTED; - enforceWritePermissionInner(documentUri); + if (!mAuthority.equals(authority)) { + throw new SecurityException( + "Requested authority " + authority + " doesn't match provider " + mAuthority); + } + enforceVia(documentUri); final Bundle out = new Bundle(); try { if (METHOD_CREATE_DOCUMENT.equals(method)) { + enforceWritePermissionInner(documentUri); + final String mimeType = extras.getString(Document.COLUMN_MIME_TYPE); final String displayName = extras.getString(Document.COLUMN_DISPLAY_NAME); final String newDocumentId = createDocument(documentId, mimeType, displayName); - out.putString(Document.COLUMN_DOCUMENT_ID, newDocumentId); - - // Extend permission grant towards caller if needed - if (!callerHasManage) { - final Uri newDocumentUri = DocumentsContract.buildDocumentUri( - mAuthority, newDocumentId); - context.grantUriPermission(getCallingPackage(), newDocumentUri, - Intent.FLAG_GRANT_READ_URI_PERMISSION - | Intent.FLAG_GRANT_WRITE_URI_PERMISSION - | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); - } + + // No need to issue new grants here, since caller either has + // manage permission or a prefix grant. We might generate a + // "via" style URI if that's how they called us. + final Uri newDocumentUri = DocumentsContract.buildDocumentMaybeViaUri(documentUri, + newDocumentId); + out.putParcelable(DocumentsContract.EXTRA_URI, newDocumentUri); } else if (METHOD_DELETE_DOCUMENT.equals(method)) { + enforceWritePermissionInner(documentUri); deleteDocument(documentId); // Document no longer exists, clean up any grants - context.revokeUriPermission(documentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION - | Intent.FLAG_GRANT_WRITE_URI_PERMISSION - | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); + revokeDocumentPermission(documentId); } else { throw new UnsupportedOperationException("Method not supported " + method); @@ -555,12 +632,25 @@ public abstract class DocumentsProvider extends ContentProvider { } /** + * Revoke any active permission grants for the given + * {@link Document#COLUMN_DOCUMENT_ID}, usually called when a document + * becomes invalid. Follows the same semantics as + * {@link Context#revokeUriPermission(Uri, int)}. + */ + public final void revokeDocumentPermission(String documentId) { + final Context context = getContext(); + context.revokeUriPermission(DocumentsContract.buildDocumentUri(mAuthority, documentId), ~0); + context.revokeUriPermission(DocumentsContract.buildViaUri(mAuthority, documentId), ~0); + } + + /** * Implementation is provided by the parent class. Cannot be overriden. * * @see #openDocument(String, String, CancellationSignal) */ @Override public final ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { + enforceVia(uri); return openDocument(getDocumentId(uri), mode, null); } @@ -572,17 +662,47 @@ public abstract class DocumentsProvider extends ContentProvider { @Override public final ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal) throws FileNotFoundException { + enforceVia(uri); return openDocument(getDocumentId(uri), mode, signal); } /** * Implementation is provided by the parent class. Cannot be overriden. * + * @see #openDocument(String, String, CancellationSignal) + */ + @Override + @SuppressWarnings("resource") + public final AssetFileDescriptor openAssetFile(Uri uri, String mode) + throws FileNotFoundException { + enforceVia(uri); + final ParcelFileDescriptor fd = openDocument(getDocumentId(uri), mode, null); + return fd != null ? new AssetFileDescriptor(fd, 0, -1) : null; + } + + /** + * Implementation is provided by the parent class. Cannot be overriden. + * + * @see #openDocument(String, String, CancellationSignal) + */ + @Override + @SuppressWarnings("resource") + public final AssetFileDescriptor openAssetFile(Uri uri, String mode, CancellationSignal signal) + throws FileNotFoundException { + enforceVia(uri); + final ParcelFileDescriptor fd = openDocument(getDocumentId(uri), mode, signal); + return fd != null ? new AssetFileDescriptor(fd, 0, -1) : null; + } + + /** + * Implementation is provided by the parent class. Cannot be overriden. + * * @see #openDocumentThumbnail(String, Point, CancellationSignal) */ @Override public final AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts) throws FileNotFoundException { + enforceVia(uri); if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) { final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE); return openDocumentThumbnail(getDocumentId(uri), sizeHint, null); @@ -600,6 +720,7 @@ public abstract class DocumentsProvider extends ContentProvider { public final AssetFileDescriptor openTypedAssetFile( Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal) throws FileNotFoundException { + enforceVia(uri); if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) { final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE); return openDocumentThumbnail(getDocumentId(uri), sizeHint, signal); diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java index 457afcc..cfab1b3 100644 --- a/core/java/android/provider/MediaStore.java +++ b/core/java/android/provider/MediaStore.java @@ -173,6 +173,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. @@ -1393,6 +1405,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; @@ -1863,6 +1880,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..ab06230 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -723,6 +723,13 @@ public final class Settings { = "android.settings.NOTIFICATION_LISTENER_SETTINGS"; /** + * @hide + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_CONDITION_PROVIDER_SETTINGS + = "android.settings.ACTION_CONDITION_PROVIDER_SETTINGS"; + + /** * Activity Action: Show settings for video captioning. * <p> * In some cases, a matching Activity may not exist, so ensure you safeguard @@ -749,6 +756,19 @@ 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"; + + /** {@hide} */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String + ACTION_SHOW_REGULATORY_INFO = "android.settings.SHOW_REGULATORY_INFO"; + // End of Intent actions for Settings /** @@ -3303,6 +3323,12 @@ public final class Settings { "input_method_selector_visibility"; /** + * The currently selected voice interaction service flattened ComponentName. + * @hide + */ + public static final String VOICE_INTERACTION_SERVICE = "voice_interaction_service"; + + /** * bluetooth HCI snoop log configuration * @hide */ @@ -3352,21 +3378,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 +3441,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 +3502,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 +3788,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 +3821,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 */ @@ -4380,6 +4528,11 @@ public final class Settings { */ public static final String ENABLED_NOTIFICATION_LISTENERS = "enabled_notification_listeners"; + /** + * @hide + */ + public static final String ENABLED_CONDITION_PROVIDERS = "enabled_condition_providers"; + /** @hide */ public static final String BAR_SERVICE_COMPONENT = "bar_service_component"; @@ -4967,6 +5120,13 @@ public final class Settings { public static final String NETWORK_PREFERENCE = "network_preference"; /** + * Which package name to use for network scoring. If null, or if the package is not a valid + * scorer app, external network scores will neither be requested nor accepted. + * @hide + */ + public static final String NETWORK_SCORER_APP = "network_scorer_app"; + + /** * If the NITZ_UPDATE_DIFF time is exceeded then an automatic adjustment * to SystemClock will be allowed even if NITZ_UPDATE_SPACING has not been * exceeded. @@ -5833,6 +5993,12 @@ public final class Settings { public static final String SHOW_PROCESSES = "show_processes"; /** + * If 1 low power mode is enabled. + * @hide + */ + public static final String LOW_POWER_MODE = "low_power"; + + /** * If 1, the activity manager will aggressively finish activities and * processes as soon as they are no longer needed. If 0, the normal * extended lifetime is used. @@ -5941,6 +6107,66 @@ 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"; + } + + /** + * Opaque value, changes when persisted zen mode configuration changes. + * + * @hide + */ + public static final String ZEN_MODE_CONFIG_ETAG = "zen_mode_config_etag"; + + /** + * 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..62252be --- /dev/null +++ b/core/java/android/provider/TvContract.java @@ -0,0 +1,596 @@ +/* + * 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.ComponentName; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.net.Uri; + +import java.util.List; + +/** + * <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"; + + private static final String PATH_CHANNEL = "channel"; + private static final String PATH_PROGRAM = "program"; + private static final String PATH_INPUT = "input"; + + /** + * An optional query, update or delete URI parameter that allows the caller to specify start + * time (in milliseconds since the epoch) to filter programs. + * + * @hide + */ + public static final String PARAM_START_TIME = "start_time"; + + /** + * An optional query, update or delete URI parameter that allows the caller to specify end time + * (in milliseconds since the epoch) to filter programs. + * + * @hide + */ + public static final String PARAM_END_TIME = "end_time"; + + /** + * A query, update or delete URI parameter that allows the caller to operate on all or + * browsable-only channels. If set to "true", the rows that contain non-browsable channels are + * not affected. + * + * @hide + */ + public static final String PARAM_BROWSABLE_ONLY = "browable_only"; + + /** + * 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 all browsable channels from a given TV input. + * + * @param name {@link ComponentName} of the {@link android.tv.TvInputService} that implements + * the given TV input. + */ + public static final Uri buildChannelsUriForInput(ComponentName name) { + return buildChannelsUriForInput(name, true); + } + + /** + * Builds a URI that points to all or browsable-only channels from a given TV input. + * + * @param name {@link ComponentName} of the {@link android.tv.TvInputService} that implements + * the given TV input. + * @param browsableOnly If set to {@code true} the URI points to only browsable channels. If set + * to {@code false} the URI points to all channels regardless of whether they are + * browsable or not. + */ + public static final Uri buildChannelsUriForInput(ComponentName name, boolean browsableOnly) { + return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(AUTHORITY) + .appendPath(PATH_INPUT).appendPath(name.getPackageName()) + .appendPath(name.getClassName()).appendPath(PATH_CHANNEL) + .appendQueryParameter(PARAM_BROWSABLE_ONLY, String.valueOf(browsableOnly)).build(); + } + + /** + * 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 all programs on a given channel. + * + * @param channelUri The URI of the channel to return programs for. + */ + public static final Uri buildProgramsUriForChannel(Uri channelUri) { + if (!PATH_CHANNEL.equals(channelUri.getPathSegments().get(0))) { + throw new IllegalArgumentException("Not a channel: " + channelUri); + } + String channelId = String.valueOf(ContentUris.parseId(channelUri)); + return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(AUTHORITY) + .appendPath(PATH_CHANNEL).appendPath(channelId).appendPath(PATH_PROGRAM).build(); + } + + /** + * Builds a URI that points to programs on a specific channel whose schedules overlap with the + * given time frame. + * + * @param channelUri The URI of the channel to return programs for. + * @param startTime The start time used to filter programs. The returned programs should have + * {@link Programs#END_TIME_UTC_MILLIS} that is greater than this time. + * @param endTime The end time used to filter programs. The returned programs should have + * {@link Programs#START_TIME_UTC_MILLIS} that is less than this time. + */ + public static final Uri buildProgramsUriForChannel(Uri channelUri, long startTime, + long endTime) { + Uri uri = buildProgramsUriForChannel(channelUri); + return uri.buildUpon().appendQueryParameter(PARAM_START_TIME, String.valueOf(startTime)) + .appendQueryParameter(PARAM_END_TIME, String.valueOf(endTime)).build(); + } + + /** + * 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); + } + + /** + * Extracts the {@link Channels#PACKAGE_NAME} from a given URI. + * + * @param channelsUri A URI constructed by {@link #buildChannelsUriForInput(ComponentName)} or + * {@link #buildChannelsUriForInput(ComponentName, boolean)}. + * @hide + */ + public static final String getPackageName(Uri channelsUri) { + final List<String> paths = channelsUri.getPathSegments(); + if (paths.size() < 4) { + throw new IllegalArgumentException("Not channels: " + channelsUri); + } + if (!PATH_INPUT.equals(paths.get(0)) || !PATH_CHANNEL.equals(paths.get(3))) { + throw new IllegalArgumentException("Not channels: " + channelsUri); + } + return paths.get(1); + } + + /** + * Extracts the {@link Channels#SERVICE_NAME} from a given URI. + * + * @param channelsUri A URI constructed by {@link #buildChannelsUriForInput(ComponentName)} or + * {@link #buildChannelsUriForInput(ComponentName, boolean)}. + * @hide + */ + public static final String getServiceName(Uri channelsUri) { + final List<String> paths = channelsUri.getPathSegments(); + if (paths.size() < 4) { + throw new IllegalArgumentException("Not channels: " + channelsUri); + } + if (!PATH_INPUT.equals(paths.get(0)) || !PATH_CHANNEL.equals(paths.get(3))) { + throw new IllegalArgumentException("Not channels: " + channelsUri); + } + return paths.get(2); + } + + /** + * Extracts the {@link Channels#_ID} from a given URI. + * + * @param programsUri A URI constructed by {@link #buildProgramsUriForChannel(Uri)} or + * {@link #buildProgramsUriForChannel(Uri, long, long)}. + * @hide + */ + public static final String getChannelId(Uri programsUri) { + final List<String> paths = programsUri.getPathSegments(); + if (paths.size() < 3) { + throw new IllegalArgumentException("Not programs: " + programsUri); + } + if (!PATH_CHANNEL.equals(paths.get(0)) || !PATH_PROGRAM.equals(paths.get(2))) { + throw new IllegalArgumentException("Not programs: " + programsUri); + } + return paths.get(1); + } + + + 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 + "/" + + PATH_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 + "/" + + PATH_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/Condition.aidl b/core/java/android/service/notification/Condition.aidl new file mode 100644 index 0000000..432852c --- /dev/null +++ b/core/java/android/service/notification/Condition.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.service.notification; + +parcelable Condition; + diff --git a/core/java/android/service/notification/Condition.java b/core/java/android/service/notification/Condition.java new file mode 100644 index 0000000..aa724f0 --- /dev/null +++ b/core/java/android/service/notification/Condition.java @@ -0,0 +1,176 @@ +/** + * 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.notification; + +import android.content.Context; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Objects; + +/** + * Condition information from condition providers. + * + * @hide + */ +public class Condition implements Parcelable { + + public static final String SCHEME = "condition"; + + public static final int STATE_FALSE = 0; + public static final int STATE_TRUE = 1; + public static final int STATE_UNKNOWN = 2; + public static final int STATE_ERROR = 3; + + public static final int FLAG_RELEVANT_NOW = 1 << 0; + public static final int FLAG_RELEVANT_ALWAYS = 1 << 1; + + public final Uri id; + public final String summary; + public final String line1; + public final String line2; + public final int icon; + public final int state; + public final int flags; + + public Condition(Uri id, String summary, String line1, String line2, int icon, + int state, int flags) { + if (id == null) throw new IllegalArgumentException("id is required"); + if (summary == null) throw new IllegalArgumentException("summary is required"); + if (line1 == null) throw new IllegalArgumentException("line1 is required"); + if (line2 == null) throw new IllegalArgumentException("line2 is required"); + if (!isValidState(state)) throw new IllegalArgumentException("state is invalid: " + state); + this.id = id; + this.summary = summary; + this.line1 = line1; + this.line2 = line2; + this.icon = icon; + this.state = state; + this.flags = flags; + } + + private Condition(Parcel source) { + this((Uri)source.readParcelable(Condition.class.getClassLoader()), + source.readString(), + source.readString(), + source.readString(), + source.readInt(), + source.readInt(), + source.readInt()); + } + + private static boolean isValidState(int state) { + return state >= STATE_FALSE && state <= STATE_ERROR; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(id, 0); + dest.writeString(summary); + dest.writeString(line1); + dest.writeString(line2); + dest.writeInt(icon); + dest.writeInt(state); + dest.writeInt(this.flags); + } + + @Override + public String toString() { + return new StringBuilder(Condition.class.getSimpleName()).append('[') + .append("id=").append(id) + .append(",summary=").append(summary) + .append(",line1=").append(line1) + .append(",line2=").append(line2) + .append(",icon=").append(icon) + .append(",state=").append(stateToString(state)) + .append(",flags=").append(flags) + .append(']').toString(); + } + + public static String stateToString(int state) { + if (state == STATE_FALSE) return "STATE_FALSE"; + if (state == STATE_TRUE) return "STATE_TRUE"; + if (state == STATE_UNKNOWN) return "STATE_UNKNOWN"; + if (state == STATE_ERROR) return "STATE_ERROR"; + throw new IllegalArgumentException("state is invalid: " + state); + } + + public static String relevanceToString(int flags) { + final boolean now = (flags & FLAG_RELEVANT_NOW) != 0; + final boolean always = (flags & FLAG_RELEVANT_ALWAYS) != 0; + if (!now && !always) return "NONE"; + if (now && always) return "NOW, ALWAYS"; + return now ? "NOW" : "ALWAYS"; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Condition)) return false; + if (o == this) return true; + final Condition other = (Condition) o; + return Objects.equals(other.id, id) + && Objects.equals(other.summary, summary) + && Objects.equals(other.line1, line1) + && Objects.equals(other.line2, line2) + && other.icon == icon + && other.state == state + && other.flags == flags; + } + + @Override + public int hashCode() { + return Objects.hash(id, summary, line1, line2, icon, state, flags); + } + + @Override + public int describeContents() { + return 0; + } + + public Condition copy() { + final Parcel parcel = Parcel.obtain(); + try { + writeToParcel(parcel, 0); + parcel.setDataPosition(0); + return new Condition(parcel); + } finally { + parcel.recycle(); + } + } + + public static Uri.Builder newId(Context context) { + return new Uri.Builder().scheme(SCHEME).authority(context.getPackageName()); + } + + public static boolean isValidId(Uri id, String pkg) { + return id != null && id.getScheme().equals(SCHEME) && id.getAuthority().equals(pkg); + } + + public static final Parcelable.Creator<Condition> CREATOR + = new Parcelable.Creator<Condition>() { + @Override + public Condition createFromParcel(Parcel source) { + return new Condition(source); + } + + @Override + public Condition[] newArray(int size) { + return new Condition[size]; + } + }; +} diff --git a/core/java/android/service/notification/ConditionProviderService.java b/core/java/android/service/notification/ConditionProviderService.java new file mode 100644 index 0000000..326412f --- /dev/null +++ b/core/java/android/service/notification/ConditionProviderService.java @@ -0,0 +1,161 @@ +/* + * 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.notification; + +import android.annotation.SdkConstant; +import android.app.INotificationManager; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.ServiceManager; +import android.util.Log; + +/** + * A service that provides conditions about boolean state. + * <p>To extend this class, you must declare the service in your manifest file with + * the {@link android.Manifest.permission#BIND_CONDITION_PROVIDER_SERVICE} permission + * and include an intent filter with the {@link #SERVICE_INTERFACE} action. For example:</p> + * <pre> + * <service android:name=".MyConditionProvider" + * android:label="@string/service_name" + * android:permission="android.permission.BIND_CONDITION_PROVIDER_SERVICE"> + * <intent-filter> + * <action android:name="android.service.notification.ConditionProviderService" /> + * </intent-filter> + * </service></pre> + * + * @hide + */ +public abstract class ConditionProviderService extends Service { + private final String TAG = ConditionProviderService.class.getSimpleName() + + "[" + getClass().getSimpleName() + "]"; + + private final H mHandler = new H(); + + private Provider mProvider; + private INotificationManager mNoMan; + + /** + * 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.notification.ConditionProviderService"; + + abstract public void onConnected(); + abstract public void onRequestConditions(int relevance); + abstract public void onSubscribe(Uri conditionId); + abstract public void onUnsubscribe(Uri conditionId); + + private final INotificationManager getNotificationInterface() { + if (mNoMan == null) { + mNoMan = INotificationManager.Stub.asInterface( + ServiceManager.getService(Context.NOTIFICATION_SERVICE)); + } + return mNoMan; + } + + public final void notifyCondition(Condition condition) { + if (condition == null) return; + notifyConditions(new Condition[]{ condition }); + } + + public final void notifyConditions(Condition... conditions) { + if (!isBound() || conditions == null) return; + try { + getNotificationInterface().notifyConditions(getPackageName(), mProvider, conditions); + } catch (android.os.RemoteException ex) { + Log.v(TAG, "Unable to contact notification manager", ex); + } + } + + @Override + public IBinder onBind(Intent intent) { + if (mProvider == null) { + mProvider = new Provider(); + } + return mProvider; + } + + private boolean isBound() { + if (mProvider == null) { + Log.w(TAG, "Condition provider service not yet bound."); + return false; + } + return true; + } + + private final class Provider extends IConditionProvider.Stub { + @Override + public void onConnected() { + mHandler.obtainMessage(H.ON_CONNECTED).sendToTarget(); + } + + @Override + public void onRequestConditions(int relevance) { + mHandler.obtainMessage(H.ON_REQUEST_CONDITIONS, relevance, 0).sendToTarget(); + } + + @Override + public void onSubscribe(Uri conditionId) { + mHandler.obtainMessage(H.ON_SUBSCRIBE, conditionId).sendToTarget(); + } + + @Override + public void onUnsubscribe(Uri conditionId) { + mHandler.obtainMessage(H.ON_UNSUBSCRIBE, conditionId).sendToTarget(); + } + } + + private final class H extends Handler { + private static final int ON_CONNECTED = 1; + private static final int ON_REQUEST_CONDITIONS = 2; + private static final int ON_SUBSCRIBE = 3; + private static final int ON_UNSUBSCRIBE = 4; + + @Override + public void handleMessage(Message msg) { + String name = null; + try { + switch(msg.what) { + case ON_CONNECTED: + name = "onConnected"; + onConnected(); + break; + case ON_REQUEST_CONDITIONS: + name = "onRequestConditions"; + onRequestConditions(msg.arg1); + break; + case ON_SUBSCRIBE: + name = "onSubscribe"; + onSubscribe((Uri)msg.obj); + break; + case ON_UNSUBSCRIBE: + name = "onUnsubscribe"; + onUnsubscribe((Uri)msg.obj); + break; + } + } catch (Throwable t) { + Log.w(TAG, "Error running " + name, t); + } + } + } +} diff --git a/core/java/android/service/notification/IConditionListener.aidl b/core/java/android/service/notification/IConditionListener.aidl new file mode 100644 index 0000000..01f874f --- /dev/null +++ b/core/java/android/service/notification/IConditionListener.aidl @@ -0,0 +1,25 @@ +/** + * 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.notification; + +import android.net.Uri; +import android.service.notification.Condition; + +/** @hide */ +oneway interface IConditionListener { + void onConditionsReceived(in Condition[] conditions); +}
\ No newline at end of file diff --git a/core/java/android/service/notification/IConditionProvider.aidl b/core/java/android/service/notification/IConditionProvider.aidl new file mode 100644 index 0000000..ada8939 --- /dev/null +++ b/core/java/android/service/notification/IConditionProvider.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.notification; + +import android.net.Uri; +import android.service.notification.Condition; + +/** @hide */ +oneway interface IConditionProvider { + void onConnected(); + void onRequestConditions(int relevance); + void onSubscribe(in Uri conditionId); + void onUnsubscribe(in Uri conditionId); +}
\ No newline at end of file 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..72720d1 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 + '|' + 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/notification/ZenModeConfig.aidl b/core/java/android/service/notification/ZenModeConfig.aidl new file mode 100644 index 0000000..c73b75e --- /dev/null +++ b/core/java/android/service/notification/ZenModeConfig.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.service.notification; + +parcelable ZenModeConfig; + diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java new file mode 100644 index 0000000..846e292 --- /dev/null +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -0,0 +1,312 @@ +/** + * 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.notification; + +import android.content.ComponentName; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Objects; + +/** + * Persisted configuration for zen mode. + * + * @hide + */ +public class ZenModeConfig implements Parcelable { + + public static final String SLEEP_MODE_NIGHTS = "nights"; + public static final String SLEEP_MODE_WEEKNIGHTS = "weeknights"; + + private static final int XML_VERSION = 1; + private static final String ZEN_TAG = "zen"; + private static final String ZEN_ATT_VERSION = "version"; + private static final String ALLOW_TAG = "allow"; + private static final String ALLOW_ATT_CALLS = "calls"; + private static final String ALLOW_ATT_MESSAGES = "messages"; + private static final String SLEEP_TAG = "sleep"; + private static final String SLEEP_ATT_MODE = "mode"; + + private static final String SLEEP_ATT_START_HR = "startHour"; + private static final String SLEEP_ATT_START_MIN = "startMin"; + private static final String SLEEP_ATT_END_HR = "endHour"; + private static final String SLEEP_ATT_END_MIN = "endMin"; + + private static final String CONDITION_TAG = "condition"; + private static final String CONDITION_ATT_COMPONENT = "component"; + private static final String CONDITION_ATT_ID = "id"; + + public boolean allowCalls; + public boolean allowMessages; + + public String sleepMode; + public int sleepStartHour; + public int sleepStartMinute; + public int sleepEndHour; + public int sleepEndMinute; + public ComponentName[] conditionComponents; + public Uri[] conditionIds; + + public ZenModeConfig() { } + + public ZenModeConfig(Parcel source) { + allowCalls = source.readInt() == 1; + allowMessages = source.readInt() == 1; + if (source.readInt() == 1) { + sleepMode = source.readString(); + } + sleepStartHour = source.readInt(); + sleepStartMinute = source.readInt(); + sleepEndHour = source.readInt(); + sleepEndMinute = source.readInt(); + int len = source.readInt(); + if (len > 0) { + conditionComponents = new ComponentName[len]; + source.readTypedArray(conditionComponents, ComponentName.CREATOR); + } + len = source.readInt(); + if (len > 0) { + conditionIds = new Uri[len]; + source.readTypedArray(conditionIds, Uri.CREATOR); + } + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(allowCalls ? 1 : 0); + dest.writeInt(allowMessages ? 1 : 0); + if (sleepMode != null) { + dest.writeInt(1); + dest.writeString(sleepMode); + } else { + dest.writeInt(0); + } + dest.writeInt(sleepStartHour); + dest.writeInt(sleepStartMinute); + dest.writeInt(sleepEndHour); + dest.writeInt(sleepEndMinute); + if (conditionComponents != null && conditionComponents.length > 0) { + dest.writeInt(conditionComponents.length); + dest.writeTypedArray(conditionComponents, 0); + } else { + dest.writeInt(0); + } + if (conditionIds != null && conditionIds.length > 0) { + dest.writeInt(conditionIds.length); + dest.writeTypedArray(conditionIds, 0); + } else { + dest.writeInt(0); + } + } + + @Override + public String toString() { + return new StringBuilder(ZenModeConfig.class.getSimpleName()).append('[') + .append("allowCalls=").append(allowCalls) + .append(",allowMessages=").append(allowMessages) + .append(",sleepMode=").append(sleepMode) + .append(",sleepStart=").append(sleepStartHour).append('.').append(sleepStartMinute) + .append(",sleepEnd=").append(sleepEndHour).append('.').append(sleepEndMinute) + .append(",conditionComponents=") + .append(conditionComponents == null ? null : TextUtils.join(",", conditionComponents)) + .append(",conditionIds=") + .append(conditionIds == null ? null : TextUtils.join(",", conditionIds)) + .append(']').toString(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof ZenModeConfig)) return false; + if (o == this) return true; + final ZenModeConfig other = (ZenModeConfig) o; + return other.allowCalls == allowCalls + && other.allowMessages == allowMessages + && Objects.equals(other.sleepMode, sleepMode) + && other.sleepStartHour == sleepStartHour + && other.sleepStartMinute == sleepStartMinute + && other.sleepEndHour == sleepEndHour + && other.sleepEndMinute == sleepEndMinute + && Objects.deepEquals(other.conditionComponents, conditionComponents) + && Objects.deepEquals(other.conditionIds, conditionIds); + } + + @Override + public int hashCode() { + return Objects.hash(allowCalls, allowMessages, sleepMode, sleepStartHour, + sleepStartMinute, sleepEndHour, sleepEndMinute, + Arrays.hashCode(conditionComponents), Arrays.hashCode(conditionIds)); + } + + public boolean isValid() { + return isValidHour(sleepStartHour) && isValidMinute(sleepStartMinute) + && isValidHour(sleepEndHour) && isValidMinute(sleepEndMinute) + && (sleepMode == null || sleepMode.equals(SLEEP_MODE_NIGHTS) + || sleepMode.equals(SLEEP_MODE_WEEKNIGHTS)); + } + + public static ZenModeConfig readXml(XmlPullParser parser) + throws XmlPullParserException, IOException { + int type = parser.getEventType(); + if (type != XmlPullParser.START_TAG) return null; + String tag = parser.getName(); + if (!ZEN_TAG.equals(tag)) return null; + final ZenModeConfig rt = new ZenModeConfig(); + final int version = Integer.parseInt(parser.getAttributeValue(null, ZEN_ATT_VERSION)); + final ArrayList<ComponentName> conditionComponents = new ArrayList<ComponentName>(); + final ArrayList<Uri> conditionIds = new ArrayList<Uri>(); + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { + tag = parser.getName(); + if (type == XmlPullParser.END_TAG && ZEN_TAG.equals(tag)) { + if (!conditionComponents.isEmpty()) { + rt.conditionComponents = conditionComponents + .toArray(new ComponentName[conditionComponents.size()]); + rt.conditionIds = conditionIds.toArray(new Uri[conditionIds.size()]); + } + return rt; + } + if (type == XmlPullParser.START_TAG) { + if (ALLOW_TAG.equals(tag)) { + rt.allowCalls = safeBoolean(parser, ALLOW_ATT_CALLS, false); + rt.allowMessages = safeBoolean(parser, ALLOW_ATT_MESSAGES, false); + } else if (SLEEP_TAG.equals(tag)) { + final String mode = parser.getAttributeValue(null, SLEEP_ATT_MODE); + rt.sleepMode = (SLEEP_MODE_NIGHTS.equals(mode) + || SLEEP_MODE_WEEKNIGHTS.equals(mode)) ? mode : null; + final int startHour = safeInt(parser, SLEEP_ATT_START_HR, 0); + final int startMinute = safeInt(parser, SLEEP_ATT_START_MIN, 0); + final int endHour = safeInt(parser, SLEEP_ATT_END_HR, 0); + final int endMinute = safeInt(parser, SLEEP_ATT_END_MIN, 0); + rt.sleepStartHour = isValidHour(startHour) ? startHour : 0; + rt.sleepStartMinute = isValidMinute(startMinute) ? startMinute : 0; + rt.sleepEndHour = isValidHour(endHour) ? endHour : 0; + rt.sleepEndMinute = isValidMinute(endMinute) ? endMinute : 0; + } else if (CONDITION_TAG.equals(tag)) { + final ComponentName component = + safeComponentName(parser, CONDITION_ATT_COMPONENT); + final Uri conditionId = safeUri(parser, CONDITION_ATT_ID); + if (component != null && conditionId != null) { + conditionComponents.add(component); + conditionIds.add(conditionId); + } + } + } + } + throw new IllegalStateException("Failed to reach END_DOCUMENT"); + } + + public void writeXml(XmlSerializer out) throws IOException { + out.startTag(null, ZEN_TAG); + out.attribute(null, ZEN_ATT_VERSION, Integer.toString(XML_VERSION)); + + out.startTag(null, ALLOW_TAG); + out.attribute(null, ALLOW_ATT_CALLS, Boolean.toString(allowCalls)); + out.attribute(null, ALLOW_ATT_MESSAGES, Boolean.toString(allowMessages)); + out.endTag(null, ALLOW_TAG); + + out.startTag(null, SLEEP_TAG); + if (sleepMode != null) { + out.attribute(null, SLEEP_ATT_MODE, sleepMode); + } + out.attribute(null, SLEEP_ATT_START_HR, Integer.toString(sleepStartHour)); + out.attribute(null, SLEEP_ATT_START_MIN, Integer.toString(sleepStartMinute)); + out.attribute(null, SLEEP_ATT_END_HR, Integer.toString(sleepEndHour)); + out.attribute(null, SLEEP_ATT_END_MIN, Integer.toString(sleepEndMinute)); + out.endTag(null, SLEEP_TAG); + + if (conditionComponents != null && conditionIds != null + && conditionComponents.length == conditionIds.length) { + for (int i = 0; i < conditionComponents.length; i++) { + out.startTag(null, CONDITION_TAG); + out.attribute(null, CONDITION_ATT_COMPONENT, + conditionComponents[i].flattenToString()); + out.attribute(null, CONDITION_ATT_ID, conditionIds[i].toString()); + out.endTag(null, CONDITION_TAG); + } + } + out.endTag(null, ZEN_TAG); + } + + public static boolean isValidHour(int val) { + return val >= 0 && val < 24; + } + + public static boolean isValidMinute(int val) { + return val >= 0 && val < 60; + } + + private static boolean safeBoolean(XmlPullParser parser, String att, boolean defValue) { + final String val = parser.getAttributeValue(null, att); + if (TextUtils.isEmpty(val)) return defValue; + return Boolean.valueOf(val); + } + + private static int safeInt(XmlPullParser parser, String att, int defValue) { + final String val = parser.getAttributeValue(null, att); + if (TextUtils.isEmpty(val)) return defValue; + return Integer.valueOf(val); + } + + private static ComponentName safeComponentName(XmlPullParser parser, String att) { + final String val = parser.getAttributeValue(null, att); + if (TextUtils.isEmpty(val)) return null; + return ComponentName.unflattenFromString(val); + } + + private static Uri safeUri(XmlPullParser parser, String att) { + final String val = parser.getAttributeValue(null, att); + if (TextUtils.isEmpty(val)) return null; + return Uri.parse(val); + } + + @Override + public int describeContents() { + return 0; + } + + public ZenModeConfig copy() { + final Parcel parcel = Parcel.obtain(); + try { + writeToParcel(parcel, 0); + parcel.setDataPosition(0); + return new ZenModeConfig(parcel); + } finally { + parcel.recycle(); + } + } + + public static final Parcelable.Creator<ZenModeConfig> CREATOR + = new Parcelable.Creator<ZenModeConfig>() { + @Override + public ZenModeConfig createFromParcel(Parcel source) { + return new ZenModeConfig(source); + } + + @Override + public ZenModeConfig[] newArray(int size) { + return new ZenModeConfig[size]; + } + }; +} 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/voice/IVoiceInteractionService.aidl b/core/java/android/service/voice/IVoiceInteractionService.aidl new file mode 100644 index 0000000..e9e2f4c --- /dev/null +++ b/core/java/android/service/voice/IVoiceInteractionService.aidl @@ -0,0 +1,23 @@ +/* + * 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.voice; + +/** + * @hide + */ +oneway interface IVoiceInteractionService { +} diff --git a/core/java/android/service/voice/IVoiceInteractionSession.aidl b/core/java/android/service/voice/IVoiceInteractionSession.aidl new file mode 100644 index 0000000..7dbf66b --- /dev/null +++ b/core/java/android/service/voice/IVoiceInteractionSession.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.voice; + +import android.os.Bundle; + +import com.android.internal.app.IVoiceInteractorCallback; +import com.android.internal.app.IVoiceInteractorRequest; + +/** + * @hide + */ +interface IVoiceInteractionSession { +} diff --git a/core/java/android/service/voice/IVoiceInteractionSessionService.aidl b/core/java/android/service/voice/IVoiceInteractionSessionService.aidl new file mode 100644 index 0000000..2519442 --- /dev/null +++ b/core/java/android/service/voice/IVoiceInteractionSessionService.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.voice; + +import android.os.Bundle; + +import android.service.voice.IVoiceInteractionSession; + +/** + * @hide + */ +oneway interface IVoiceInteractionSessionService { + void newSession(IBinder token, in Bundle args); +} diff --git a/core/java/android/service/voice/VoiceInteractionService.java b/core/java/android/service/voice/VoiceInteractionService.java new file mode 100644 index 0000000..d005890 --- /dev/null +++ b/core/java/android/service/voice/VoiceInteractionService.java @@ -0,0 +1,77 @@ +/** + * 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.voice; + +import android.annotation.SdkConstant; +import android.app.Instrumentation; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; +import com.android.internal.app.IVoiceInteractionManagerService; + +public class VoiceInteractionService extends Service { + /** + * The {@link Intent} that must be declared as handled by the service. + * To be supported, the service must also require the + * {@link android.Manifest.permission#BIND_VOICE_INTERACTION} permission so + * that other applications can not abuse it. + */ + @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) + public static final String SERVICE_INTERFACE = + "android.service.voice.VoiceInteractionService"; + + /** + * Name under which a VoiceInteractionService component publishes information about itself. + * This meta-data should reference an XML resource containing a + * <code><{@link + * android.R.styleable#VoiceInteractionService voice-interaction-service}></code> tag. + */ + public static final String SERVICE_META_DATA = "android.voice_interaction"; + + IVoiceInteractionService mInterface = new IVoiceInteractionService.Stub() { + }; + + IVoiceInteractionManagerService mSystemService; + + public void startVoiceActivity(Intent intent, Bundle sessionArgs) { + try { + mSystemService.startVoiceActivity(intent, + intent.resolveType(getContentResolver()), + mInterface, sessionArgs); + } catch (RemoteException e) { + } + } + + @Override + public void onCreate() { + super.onCreate(); + mSystemService = IVoiceInteractionManagerService.Stub.asInterface( + ServiceManager.getService(Context.VOICE_INTERACTION_MANAGER_SERVICE)); + } + + @Override + public IBinder onBind(Intent intent) { + if (SERVICE_INTERFACE.equals(intent.getAction())) { + return mInterface.asBinder(); + } + return null; + } +} diff --git a/core/java/android/service/voice/VoiceInteractionServiceInfo.java b/core/java/android/service/voice/VoiceInteractionServiceInfo.java new file mode 100644 index 0000000..a909ead --- /dev/null +++ b/core/java/android/service/voice/VoiceInteractionServiceInfo.java @@ -0,0 +1,125 @@ +/* + * 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.voice; + +import android.Manifest; +import android.content.ComponentName; +import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.speech.RecognitionService; +import android.util.AttributeSet; +import android.util.Log; +import android.util.Xml; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; + +/** @hide */ +public class VoiceInteractionServiceInfo { + static final String TAG = "VoiceInteractionServiceInfo"; + + private String mParseError; + + private ServiceInfo mServiceInfo; + private String mSessionService; + private String mSettingsActivity; + + public VoiceInteractionServiceInfo(PackageManager pm, ComponentName comp) + throws PackageManager.NameNotFoundException { + this(pm, pm.getServiceInfo(comp, PackageManager.GET_META_DATA)); + } + + public VoiceInteractionServiceInfo(PackageManager pm, ServiceInfo si) { + if (!Manifest.permission.BIND_VOICE_INTERACTION.equals(si.permission)) { + mParseError = "Service does not require permission " + + Manifest.permission.BIND_VOICE_INTERACTION; + return; + } + + XmlResourceParser parser = null; + try { + parser = si.loadXmlMetaData(pm, VoiceInteractionService.SERVICE_META_DATA); + if (parser == null) { + mParseError = "No " + VoiceInteractionService.SERVICE_META_DATA + + " meta-data for " + si.packageName; + return; + } + + 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 (!"voice-interaction-service".equals(nodeName)) { + mParseError = "Meta-data does not start with voice-interaction-service tag"; + return; + } + + TypedArray array = res.obtainAttributes(attrs, + com.android.internal.R.styleable.VoiceInteractionService); + mSessionService = array.getString( + com.android.internal.R.styleable.VoiceInteractionService_sessionService); + mSettingsActivity = array.getString( + com.android.internal.R.styleable.VoiceInteractionService_settingsActivity); + array.recycle(); + if (mSessionService == null) { + mParseError = "No sessionService specified"; + return; + } + } catch (XmlPullParserException e) { + mParseError = "Error parsing voice interation service meta-data: " + e; + Log.w(TAG, "error parsing voice interaction service meta-data", e); + return; + } catch (IOException e) { + mParseError = "Error parsing voice interation service meta-data: " + e; + Log.w(TAG, "error parsing voice interaction service meta-data", e); + return; + } catch (PackageManager.NameNotFoundException e) { + mParseError = "Error parsing voice interation service meta-data: " + e; + Log.w(TAG, "error parsing voice interaction service meta-data", e); + return; + } finally { + if (parser != null) parser.close(); + } + mServiceInfo = si; + } + + public String getParseError() { + return mParseError; + } + + public ServiceInfo getServiceInfo() { + return mServiceInfo; + } + + public String getSessionService() { + return mSessionService; + } + + public String getSettingsActivity() { + return mSettingsActivity; + } +} diff --git a/core/java/android/service/voice/VoiceInteractionSession.java b/core/java/android/service/voice/VoiceInteractionSession.java new file mode 100644 index 0000000..963b6b4 --- /dev/null +++ b/core/java/android/service/voice/VoiceInteractionSession.java @@ -0,0 +1,195 @@ +/** + * 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.voice; + +import android.content.Context; +import android.os.Binder; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.RemoteException; +import android.util.ArrayMap; +import android.util.Log; +import com.android.internal.app.IVoiceInteractor; +import com.android.internal.app.IVoiceInteractorCallback; +import com.android.internal.app.IVoiceInteractorRequest; +import com.android.internal.os.HandlerCaller; +import com.android.internal.os.SomeArgs; + +public abstract class VoiceInteractionSession { + static final String TAG = "VoiceInteractionSession"; + static final boolean DEBUG = true; + + final IVoiceInteractor mInteractor = new IVoiceInteractor.Stub() { + @Override + public IVoiceInteractorRequest startConfirmation(String callingPackage, + IVoiceInteractorCallback callback, String prompt, Bundle extras) { + Request request = findRequest(callback, true); + mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageOOOO(MSG_START_CONFIRMATION, + new Caller(callingPackage, Binder.getCallingUid()), request, + prompt, extras)); + return request.mInterface; + } + + @Override + public IVoiceInteractorRequest startCommand(String callingPackage, + IVoiceInteractorCallback callback, String command, Bundle extras) { + Request request = findRequest(callback, true); + mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageOOOO(MSG_START_COMMAND, + new Caller(callingPackage, Binder.getCallingUid()), request, + command, extras)); + return request.mInterface; + } + + @Override + public boolean[] supportsCommands(String callingPackage, String[] commands) { + Message msg = mHandlerCaller.obtainMessageIOO(MSG_SUPPORTS_COMMANDS, + 0, new Caller(callingPackage, Binder.getCallingUid()), commands); + SomeArgs args = mHandlerCaller.sendMessageAndWait(msg); + if (args != null) { + boolean[] res = (boolean[])args.arg1; + args.recycle(); + return res; + } + return new boolean[commands.length]; + } + }; + + final IVoiceInteractionSession mSession = new IVoiceInteractionSession.Stub() { + }; + + public static class Request { + final IVoiceInteractorRequest mInterface = new IVoiceInteractorRequest.Stub() { + @Override + public void cancel() throws RemoteException { + mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageO(MSG_CANCEL, Request.this)); + } + }; + final IVoiceInteractorCallback mCallback; + final HandlerCaller mHandlerCaller; + Request(IVoiceInteractorCallback callback, HandlerCaller handlerCaller) { + mCallback = callback; + mHandlerCaller = handlerCaller; + } + + public void sendConfirmResult(boolean confirmed, Bundle result) { + try { + if (DEBUG) Log.d(TAG, "sendConfirmResult: req=" + mInterface + + " confirmed=" + confirmed + " result=" + result); + mCallback.deliverConfirmationResult(mInterface, confirmed, result); + } catch (RemoteException e) { + } + } + + public void sendCommandResult(boolean complete, Bundle result) { + try { + if (DEBUG) Log.d(TAG, "sendCommandResult: req=" + mInterface + + " result=" + result); + mCallback.deliverCommandResult(mInterface, complete, result); + } catch (RemoteException e) { + } + } + + public void sendCancelResult() { + try { + if (DEBUG) Log.d(TAG, "sendCancelResult: req=" + mInterface); + mCallback.deliverCancel(mInterface); + } catch (RemoteException e) { + } + } + } + + public static class Caller { + final String packageName; + final int uid; + + Caller(String _packageName, int _uid) { + packageName = _packageName; + uid = _uid; + } + } + + static final int MSG_START_CONFIRMATION = 1; + static final int MSG_START_COMMAND = 2; + static final int MSG_SUPPORTS_COMMANDS = 3; + static final int MSG_CANCEL = 4; + + final Context mContext; + final HandlerCaller mHandlerCaller; + final HandlerCaller.Callback mHandlerCallerCallback = new HandlerCaller.Callback() { + @Override + public void executeMessage(Message msg) { + SomeArgs args = (SomeArgs)msg.obj; + switch (msg.what) { + case MSG_START_CONFIRMATION: + if (DEBUG) Log.d(TAG, "onConfirm: req=" + ((Request) args.arg2).mInterface + + " prompt=" + args.arg3 + " extras=" + args.arg4); + onConfirm((Caller)args.arg1, (Request)args.arg2, (String)args.arg3, + (Bundle)args.arg4); + break; + case MSG_START_COMMAND: + if (DEBUG) Log.d(TAG, "onCommand: req=" + ((Request) args.arg2).mInterface + + " command=" + args.arg3 + " extras=" + args.arg4); + onCommand((Caller) args.arg1, (Request) args.arg2, (String) args.arg3, + (Bundle) args.arg4); + break; + case MSG_SUPPORTS_COMMANDS: + if (DEBUG) Log.d(TAG, "onGetSupportedCommands: cmds=" + args.arg2); + args.arg1 = onGetSupportedCommands((Caller) args.arg1, (String[]) args.arg2); + break; + case MSG_CANCEL: + if (DEBUG) Log.d(TAG, "onCancel: req=" + ((Request) args.arg1).mInterface); + onCancel((Request)args.arg1); + break; + } + } + }; + + final ArrayMap<IBinder, Request> mActiveRequests = new ArrayMap<IBinder, Request>(); + + public VoiceInteractionSession(Context context) { + this(context, new Handler()); + } + + public VoiceInteractionSession(Context context, Handler handler) { + mContext = context; + mHandlerCaller = new HandlerCaller(context, handler.getLooper(), + mHandlerCallerCallback, true); + } + + Request findRequest(IVoiceInteractorCallback callback, boolean newRequest) { + synchronized (this) { + Request req = mActiveRequests.get(callback.asBinder()); + if (req != null) { + if (newRequest) { + throw new IllegalArgumentException("Given request callback " + callback + + " is already active"); + } + return req; + } + req = new Request(callback, mHandlerCaller); + mActiveRequests.put(callback.asBinder(), req); + return req; + } + } + + public abstract boolean[] onGetSupportedCommands(Caller caller, String[] commands); + public abstract void onConfirm(Caller caller, Request request, String prompt, Bundle extras); + public abstract void onCommand(Caller caller, Request request, String command, Bundle extras); + public abstract void onCancel(Request request); +} diff --git a/core/java/android/service/voice/VoiceInteractionSessionService.java b/core/java/android/service/voice/VoiceInteractionSessionService.java new file mode 100644 index 0000000..40e5bba --- /dev/null +++ b/core/java/android/service/voice/VoiceInteractionSessionService.java @@ -0,0 +1,82 @@ +/** + * 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.voice; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.os.ServiceManager; +import com.android.internal.app.IVoiceInteractionManagerService; +import com.android.internal.os.HandlerCaller; +import com.android.internal.os.SomeArgs; + +public abstract class VoiceInteractionSessionService extends Service { + + static final int MSG_NEW_SESSION = 1; + + IVoiceInteractionManagerService mSystemService; + + IVoiceInteractionSessionService mInterface = new IVoiceInteractionSessionService.Stub() { + public void newSession(IBinder token, Bundle args) { + mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageOO(MSG_NEW_SESSION, + token, args)); + + } + }; + + HandlerCaller mHandlerCaller; + final HandlerCaller.Callback mHandlerCallerCallback = new HandlerCaller.Callback() { + @Override + public void executeMessage(Message msg) { + SomeArgs args = (SomeArgs)msg.obj; + switch (msg.what) { + case MSG_NEW_SESSION: + doNewSession((IBinder)args.arg1, (Bundle)args.arg2); + break; + } + } + }; + + @Override + public void onCreate() { + super.onCreate(); + mSystemService = IVoiceInteractionManagerService.Stub.asInterface( + ServiceManager.getService(Context.VOICE_INTERACTION_MANAGER_SERVICE)); + mHandlerCaller = new HandlerCaller(this, Looper.myLooper(), + mHandlerCallerCallback, true); + } + + public abstract VoiceInteractionSession onNewSession(Bundle args); + + @Override + public IBinder onBind(Intent intent) { + return mInterface.asBinder(); + } + + void doNewSession(IBinder token, Bundle args) { + VoiceInteractionSession session = onNewSession(args); + try { + mSystemService.deliverNewSession(token, session.mSession, session.mInteractor); + } catch (RemoteException e) { + } + } +} 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 814326c..0db00f0 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/format/Formatter.java b/core/java/android/text/format/Formatter.java index 9c98b98..b0cbcd2 100644 --- a/core/java/android/text/format/Formatter.java +++ b/core/java/android/text/format/Formatter.java @@ -106,4 +106,69 @@ public final class Formatter { public static String formatIpAddress(int ipv4Address) { return NetworkUtils.intToInetAddress(ipv4Address).getHostAddress(); } + + private static final int SECONDS_PER_MINUTE = 60; + private static final int SECONDS_PER_HOUR = 60 * 60; + private static final int SECONDS_PER_DAY = 24 * 60 * 60; + + /** + * Returns elapsed time for the given millis, in the following format: + * 1 day 5 hrs; will include at most two units, can go down to seconds precision. + * @param context the application context + * @param millis the elapsed time in milli seconds + * @return the formatted elapsed time + * @hide + */ + public static String formatShortElapsedTime(Context context, long millis) { + long secondsLong = millis / 1000; + + int days = 0, hours = 0, minutes = 0; + if (secondsLong >= SECONDS_PER_DAY) { + days = (int)(secondsLong / SECONDS_PER_DAY); + secondsLong -= days * SECONDS_PER_DAY; + } + if (secondsLong >= SECONDS_PER_HOUR) { + hours = (int)(secondsLong / SECONDS_PER_HOUR); + secondsLong -= hours * SECONDS_PER_HOUR; + } + if (secondsLong >= SECONDS_PER_MINUTE) { + minutes = (int)(secondsLong / SECONDS_PER_MINUTE); + secondsLong -= minutes * SECONDS_PER_MINUTE; + } + int seconds = (int)secondsLong; + + if (days >= 2) { + days += (hours+12)/24; + return context.getString(com.android.internal.R.string.durationDays, days); + } else if (days > 0) { + if (hours == 1) { + return context.getString(com.android.internal.R.string.durationDayHour, days, hours); + } + return context.getString(com.android.internal.R.string.durationDayHours, days, hours); + } else if (hours >= 2) { + hours += (minutes+30)/60; + return context.getString(com.android.internal.R.string.durationHours, hours); + } else if (hours > 0) { + if (minutes == 1) { + return context.getString(com.android.internal.R.string.durationHourMinute, hours, + minutes); + } + return context.getString(com.android.internal.R.string.durationHourMinutes, hours, + minutes); + } else if (minutes >= 2) { + minutes += (seconds+30)/60; + return context.getString(com.android.internal.R.string.durationMinutes, minutes); + } else if (minutes > 0) { + if (seconds == 1) { + return context.getString(com.android.internal.R.string.durationMinuteSecond, minutes, + seconds); + } + return context.getString(com.android.internal.R.string.durationMinuteSeconds, minutes, + seconds); + } else if (seconds == 1) { + return context.getString(com.android.internal.R.string.durationSecond, seconds); + } else { + return context.getString(com.android.internal.R.string.durationSeconds, seconds); + } + } } 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/ChangeClipBounds.java b/core/java/android/transition/ChangeClipBounds.java new file mode 100644 index 0000000..a61b29d --- /dev/null +++ b/core/java/android/transition/ChangeClipBounds.java @@ -0,0 +1,96 @@ +/* + * 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.ObjectAnimator; +import android.animation.RectEvaluator; +import android.graphics.Rect; +import android.view.View; +import android.view.ViewGroup; + +/** + * ChangeClipBounds captures the {@link android.view.View#getClipBounds()} before and after the + * scene change and animates those changes during the transition. + */ +public class ChangeClipBounds extends Transition { + + private static final String TAG = "ChangeTransform"; + + private static final String PROPNAME_CLIP = "android:clipBounds:clip"; + private static final String PROPNAME_BOUNDS = "android:clipBounds:bounds"; + + private static final String[] sTransitionProperties = { + PROPNAME_CLIP, + }; + + @Override + public String[] getTransitionProperties() { + return sTransitionProperties; + } + + private void captureValues(TransitionValues values) { + View view = values.view; + if (view.getVisibility() == View.GONE) { + return; + } + + Rect clip = view.getClipBounds(); + values.values.put(PROPNAME_CLIP, clip); + if (clip == null) { + Rect bounds = new Rect(0, 0, view.getWidth(), view.getHeight()); + values.values.put(PROPNAME_BOUNDS, bounds); + } + } + + @Override + public void captureStartValues(TransitionValues transitionValues) { + captureValues(transitionValues); + } + + @Override + public void captureEndValues(TransitionValues transitionValues) { + captureValues(transitionValues); + } + + @Override + public Animator createAnimator(final ViewGroup sceneRoot, TransitionValues startValues, + TransitionValues endValues) { + if (startValues == null || endValues == null + || !startValues.values.containsKey(PROPNAME_CLIP) + || !endValues.values.containsKey(PROPNAME_CLIP)) { + return null; + } + Rect start = (Rect) startValues.values.get(PROPNAME_CLIP); + Rect end = (Rect) endValues.values.get(PROPNAME_CLIP); + if (start == null && end == null) { + return null; // No animation required since there is no clip. + } + + if (start == null) { + start = (Rect) startValues.values.get(PROPNAME_BOUNDS); + } else if (end == null) { + end = (Rect) endValues.values.get(PROPNAME_BOUNDS); + } + if (start.equals(end)) { + return null; + } + + endValues.view.setClipBounds(start); + RectEvaluator evaluator = new RectEvaluator(new Rect()); + return ObjectAnimator.ofObject(endValues.view, "clipBounds", evaluator, start, end); + } +} diff --git a/core/java/android/transition/ChangeTransform.java b/core/java/android/transition/ChangeTransform.java new file mode 100644 index 0000000..85cb2c7 --- /dev/null +++ b/core/java/android/transition/ChangeTransform.java @@ -0,0 +1,163 @@ +/* + * 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.FloatArrayEvaluator; +import android.animation.ObjectAnimator; +import android.util.FloatProperty; +import android.util.Property; +import android.view.View; +import android.view.ViewGroup; + +/** + * This Transition captures scale and rotation for Views before and after the + * scene change and animates those changes during the transition. + * + * <p>ChangeTransform does not work when the pivot changes between scenes, so either the + * pivot must be set to prevent automatic pivot adjustment or the View's size must be unchanged.</p> + */ +public class ChangeTransform extends Transition { + + private static final String TAG = "ChangeTransform"; + + private static final String PROPNAME_SCALE_X = "android:changeTransform:scaleX"; + private static final String PROPNAME_SCALE_Y = "android:changeTransform:scaleY"; + private static final String PROPNAME_ROTATION_X = "android:changeTransform:rotationX"; + private static final String PROPNAME_ROTATION_Y = "android:changeTransform:rotationY"; + private static final String PROPNAME_ROTATION_Z = "android:changeTransform:rotationZ"; + private static final String PROPNAME_PIVOT_X = "android:changeTransform:pivotX"; + private static final String PROPNAME_PIVOT_Y = "android:changeTransform:pivotY"; + + private static final String[] sTransitionProperties = { + PROPNAME_SCALE_X, + PROPNAME_SCALE_Y, + PROPNAME_ROTATION_X, + PROPNAME_ROTATION_Y, + PROPNAME_ROTATION_Z, + }; + + private static final FloatProperty<View>[] sChangedProperties = new FloatProperty[] { + (FloatProperty) View.SCALE_X, + (FloatProperty) View.SCALE_Y, + (FloatProperty) View.ROTATION_X, + (FloatProperty) View.ROTATION_Y, + (FloatProperty) View.ROTATION, + }; + + private static Property<View, float[]> TRANSFORMS = new Property<View, float[]>(float[].class, + "transforms") { + @Override + public float[] get(View object) { + return null; + } + + @Override + public void set(View view, float[] values) { + for (int i = 0; i < values.length; i++) { + float value = values[i]; + if (value != Float.NaN) { + sChangedProperties[i].setValue(view, value); + } + } + } + }; + + @Override + public String[] getTransitionProperties() { + return sTransitionProperties; + } + + private void captureValues(TransitionValues values) { + View view = values.view; + if (view.getVisibility() == View.GONE) { + return; + } + + values.values.put(PROPNAME_SCALE_X, view.getScaleX()); + values.values.put(PROPNAME_SCALE_Y, view.getScaleY()); + values.values.put(PROPNAME_PIVOT_X, view.getPivotX()); + values.values.put(PROPNAME_PIVOT_Y, view.getPivotY()); + values.values.put(PROPNAME_ROTATION_X, view.getRotationX()); + values.values.put(PROPNAME_ROTATION_Y, view.getRotationY()); + values.values.put(PROPNAME_ROTATION_Z, view.getRotation()); + } + + @Override + public void captureStartValues(TransitionValues transitionValues) { + captureValues(transitionValues); + } + + @Override + public void captureEndValues(TransitionValues transitionValues) { + captureValues(transitionValues); + } + + @Override + public Animator createAnimator(final ViewGroup sceneRoot, TransitionValues startValues, + TransitionValues endValues) { + if (startValues == null || endValues == null + || !startValues.values.containsKey(PROPNAME_SCALE_X) + || !endValues.values.containsKey(PROPNAME_SCALE_X) + || !isPivotSame(startValues, endValues) + || !isChanged(startValues, endValues)) { + return null; + } + + float[] start = createValues(startValues); + float[] end = createValues(endValues); + for (int i = 0; i < start.length; i++) { + if (start[i] == end[i]) { + start[i] = Float.NaN; + end[i] = Float.NaN; + } else { + sChangedProperties[i].setValue(endValues.view, start[i]); + } + } + FloatArrayEvaluator evaluator = new FloatArrayEvaluator(new float[start.length]); + return ObjectAnimator.ofObject(endValues.view, TRANSFORMS, evaluator, start, end); + } + + private static float[] createValues(TransitionValues transitionValues) { + float[] values = new float[sChangedProperties.length]; + for (int i = 0; i < values.length; i++) { + values[i] = (Float) transitionValues.values.get(sTransitionProperties[i]); + } + return values; + } + + private static boolean isPivotSame(TransitionValues startValues, TransitionValues endValues) { + float startPivotX = (Float) startValues.values.get(PROPNAME_PIVOT_X); + float startPivotY = (Float) startValues.values.get(PROPNAME_PIVOT_Y); + float endPivotX = (Float) endValues.values.get(PROPNAME_PIVOT_X); + float endPivotY = (Float) endValues.values.get(PROPNAME_PIVOT_Y); + + // We don't support pivot changes, because they could be automatically set + // and we can't end the state in an automatic state. + return startPivotX == endPivotX && startPivotY == endPivotY; + } + + private static boolean isChanged(TransitionValues startValues, TransitionValues endValues) { + for (int i = 0; i < sChangedProperties.length; i++) { + Object start = startValues.values.get(sTransitionProperties[i]); + Object end = endValues.values.get(sTransitionProperties[i]); + if (!start.equals(end)) { + return true; + } + } + return false; + } +} diff --git a/core/java/android/transition/CircularPropagation.java b/core/java/android/transition/CircularPropagation.java new file mode 100644 index 0000000..51beb51 --- /dev/null +++ b/core/java/android/transition/CircularPropagation.java @@ -0,0 +1,107 @@ +/* + * 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 = 3.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; + + long duration = transition.getDuration(); + if (duration < 0) { + duration = 300; + } + + return Math.round(duration * 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..e70dc0c 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,79 @@ 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); + 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 boolean mCanceled = false; + private float mPausedAlpha = -1; - @Override - public void onAnimationResume(Animator animation) { - if (finalOverlayView != null) { - finalSceneRoot.getOverlay().add(finalOverlayView); - } - } - }; - return createAnimation(view, startAlpha, endAlpha, endListener); + public FadeAnimatorListener(View view) { + mView = view; } - 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(1); + } + } - @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(1); + } - @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..e4c3939 --- /dev/null +++ b/core/java/android/transition/MoveImage.java @@ -0,0 +1,338 @@ +/* + * 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); + final ImageView overlayImage = new ImageView(imageView.getContext()); + final ViewGroupOverlay overlay = sceneRoot.getOverlay(); + overlay.add(overlayImage); + overlayImage.setLeft(0); + overlayImage.setTop(0); + overlayImage.setRight(sceneRoot.getWidth()); + overlayImage.setBottom(sceneRoot.getBottom()); + overlayImage.setScaleType(ImageView.ScaleType.MATRIX); + overlayImage.setImageDrawable(matrixClippedDrawable); + matrixClippedDrawable.setMatrix(startMatrix); + matrixClippedDrawable.setBounds(startBounds); + matrixClippedDrawable.setClipRect(startClip); + + imageView.setVisibility(View.INVISIBLE); + 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(overlayImage); + } + + @Override + public void onAnimationPause(Animator animation) { + imageView.setVisibility(View.VISIBLE); + overlayImage.setVisibility(View.INVISIBLE); + } + + @Override + public void onAnimationResume(Animator animation) { + imageView.setVisibility(View.INVISIBLE); + overlayImage.setVisibility(View.VISIBLE); + } + + @Override + public void onAnimationCancel(Animator animation) { + onAnimationEnd(animation); + } + }; + + 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..5d38ac8 --- /dev/null +++ b/core/java/android/transition/SidePropagation.java @@ -0,0 +1,169 @@ +/* + * 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 = 3.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; + + long duration = transition.getDuration(); + if (duration < 0) { + duration = 300; + } + + return Math.round(duration * 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..2549fde 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,17 @@ 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>This TransitionSet contains {@link android.transition.Explode} for visibility, + * {@link android.transition.ChangeBounds}, {@link android.transition.ChangeTransform}, + * and {@link android.transition.ChangeClipBounds} for non-<code>ImageView</code>s and + * {@link android.transition.MoveImage} for <code>ImageView</code>s:</p> + * + * {@sample development/samples/ApiDemos/res/transition/explode_move_together.xml MultipleTransform} + * * <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 @@ -78,7 +88,8 @@ import java.util.List; * transition uses a fadingMode of {@link Fade#OUT} instead of the default * out-in behavior. Finally, note the use of the <code>targets</code> sub-tag, which * takes a set of {@link android.R.styleable#TransitionTarget target} tags, each - * of which lists a specific <code>targetId</code> which this transition acts upon. + * of which lists a specific <code>targetId</code>, <code>targetClass</code>, + * <code>excludeId</code>, or <code>excludeClass</code>, which this transition acts upon. * Use of targets is optional, but can be used to either limit the time spent checking * attributes on unchanging views, or limiting the types of animations run on specific views. * In this case, we know that only the <code>grayscaleContainer</code> will be @@ -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 { @@ -104,6 +116,7 @@ public abstract class Transition implements Cloneable { ArrayList<Integer> mTargetIdExcludes = null; ArrayList<View> mTargetExcludes = null; ArrayList<Class> mTargetTypeExcludes = null; + ArrayList<Class> mTargetTypes = null; ArrayList<Integer> mTargetIdChildExcludes = null; ArrayList<View> mTargetChildExcludes = null; ArrayList<Class> mTargetTypeChildExcludes = null; @@ -148,6 +161,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 +454,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 +519,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 +534,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); + } + } } /** @@ -532,19 +570,15 @@ public abstract class Transition implements Cloneable { } } } - if (mTargetIds.size() == 0 && mTargets.size() == 0) { + if (mTargetIds.size() == 0 && mTargets.size() == 0 && mTargetTypes == null) { return true; } - if (mTargetIds.size() > 0) { - for (int i = 0; i < mTargetIds.size(); ++i) { - if (mTargetIds.get(i) == targetId) { - return true; - } - } + if (mTargetIds.contains((int) targetId) || mTargets.contains(target)) { + return true; } - if (target != null && mTargets.size() > 0) { - for (int i = 0; i < mTargets.size(); ++i) { - if (mTargets.get(i) == target) { + if (mTargetTypes != null) { + for (int i = 0; i < mTargetTypes.size(); ++i) { + if (mTargetTypes.get(i).isInstance(target)) { return true; } } @@ -563,7 +597,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 */ @@ -690,6 +724,36 @@ public abstract class Transition implements Cloneable { } /** + * Adds the Class of a target view that this Transition is interested in + * animating. By default, there are no targetTypes, and a Transition will + * listen for changes on every view in the hierarchy below the sceneRoot + * of the Scene being transitioned into. Setting targetTypes constrains + * the Transition to only listen for, and act on, views with these classes. + * Views with different classes will be ignored. + * + * <p>Note that any View that can be cast to targetType will be included, so + * if targetType is <code>View.class</code>, all Views will be included.</p> + * + * @see #addTarget(int) + * @see #addTarget(android.view.View) + * @see #excludeTarget(Class, boolean) + * @see #excludeChildren(Class, boolean) + * + * @param targetType The type to include when running this transition. + * @return The Transition to which the target class was added. + * Returning the same object makes it easier to chain calls during + * construction, such as + * <code>transitionSet.addTransitions(new Fade()).addTarget(ImageView.class);</code> + */ + public Transition addTarget(Class targetType) { + if (mTargetTypes == null) { + mTargetTypes = new ArrayList<Class>(); + } + mTargetTypes.add(targetType); + return this; + } + + /** * Removes the given targetId from the list of ids that this Transition * is interested in animating. * @@ -1008,6 +1072,7 @@ public abstract class Transition implements Cloneable { } else { captureEndValues(values); } + capturePropagationValues(values); if (start) { mStartValues.viewValues.put(view, values); if (id >= 0) { @@ -1033,6 +1098,7 @@ public abstract class Transition implements Cloneable { } else { captureEndValues(values); } + capturePropagationValues(values); if (start) { mStartValues.viewValues.put(view, values); } else { @@ -1109,30 +1175,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 +1263,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 +1294,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 +1402,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 +1535,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 && !transitionValues.values.isEmpty()) { + 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 +1636,10 @@ public abstract class Transition implements Cloneable { mCanRemoveViews = canRemoveViews; } + public boolean canRemoveViews() { + return mCanRemoveViews; + } + @Override public String toString() { return toString(""); @@ -1629,16 +1802,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 +1864,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..a5e960a 100644 --- a/core/java/android/transition/TransitionInflater.java +++ b/core/java/android/transition/TransitionInflater.java @@ -145,7 +145,19 @@ 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 ("changeTransform".equals(name)) { + transition = new ChangeTransform(); + newTransition = true; + } else if ("changeClipBounds".equals(name)) { + transition = new ChangeClipBounds(); newTransition = true; } else if ("autoTransition".equals(name)) { transition = new AutoTransition(); @@ -188,6 +200,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 { @@ -195,7 +216,6 @@ public class TransitionInflater { int type; int depth = parser.getDepth(); - ArrayList<Integer> targetIds = new ArrayList<Integer>(); while (((type=parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { @@ -210,18 +230,31 @@ public class TransitionInflater { int id = a.getResourceId( com.android.internal.R.styleable.TransitionTarget_targetId, -1); if (id >= 0) { - targetIds.add(id); + transition.addTarget(id); + } else if ((id = a.getResourceId( + com.android.internal.R.styleable.TransitionTarget_excludeId, -1)) >= 0) { + transition.excludeTarget(id, true); + } else { + String className = a.getString( + com.android.internal.R.styleable.TransitionTarget_excludeClass); + try { + if (className != null) { + Class clazz = Class.forName(className); + transition.excludeTarget(clazz, true); + } else if ((className = a.getString( + com.android.internal.R.styleable.TransitionTarget_targetClass)) + != null) { + Class clazz = Class.forName(className); + transition.addTarget(clazz); + } + } catch (ClassNotFoundException e) { + throw new RuntimeException("Could not create " + className, e); + } } } else { throw new RuntimeException("Unknown scene name: " + parser.getName()); } } - int numTargets = targetIds.size(); - if (numTargets > 0) { - for (int i = 0; i < numTargets; ++i) { - transition.addTarget(targetIds.get(i)); - } - } } private Transition loadTransition(Transition transition, AttributeSet attrs) @@ -284,25 +317,23 @@ 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 " + - "for transition ID " + transitionId); - } else { - transitionManager.setTransition(fromScene, toScene, transition); - } - } else if (toId >= 0) { + if (toScene == null) { + throw new RuntimeException("No toScene for transition ID " + transitionId); + } + if (fromScene == null) { transitionManager.setTransition(toScene, 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..ce3cc2f 100644 --- a/core/java/android/transition/TransitionManager.java +++ b/core/java/android/transition/TransitionManager.java @@ -67,6 +67,8 @@ 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, ArrayMap<Scene, Transition>> mScenePairTransitions = new ArrayMap<Scene, ArrayMap<Scene, Transition>>(); @@ -253,7 +255,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 +288,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 +304,7 @@ public class TransitionManager { if (runningTransitions != null && runningTransitions.size() > 0) { for (Transition runningTransition : runningTransitions) { - runningTransition.pause(); + runningTransition.pause(sceneRoot); } } @@ -329,7 +331,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..9081234 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; @@ -255,11 +256,45 @@ public class TransitionSet extends Transition { @Override protected void createAnimators(ViewGroup sceneRoot, TransitionValuesMaps startValues, TransitionValuesMaps endValues) { + startValues = removeExcludes(startValues); + endValues = removeExcludes(endValues); for (Transition childTransition : mTransitions) { childTransition.createAnimators(sceneRoot, startValues, endValues); } } + private TransitionValuesMaps removeExcludes(TransitionValuesMaps values) { + if (mTargetIds.isEmpty() && mTargetIdExcludes == null && mTargetTypeExcludes == null + && mTargets.isEmpty()) { + return values; + } + TransitionValuesMaps included = new TransitionValuesMaps(); + int numValues = values.viewValues.size(); + for (int i = 0; i < numValues; i++) { + View view = values.viewValues.keyAt(i); + if (isValidTarget(view, view.getId())) { + included.viewValues.put(view, values.viewValues.valueAt(i)); + } + } + numValues = values.idValues.size(); + for (int i = 0; i < numValues; i++) { + int id = values.idValues.keyAt(i); + TransitionValues transitionValues = values.idValues.valueAt(i); + if (isValidTarget(transitionValues.view, id)) { + included.idValues.put(id, transitionValues); + } + } + numValues = values.itemIdValues.size(); + for (int i = 0; i < numValues; i++) { + long id = values.itemIdValues.keyAt(i); + TransitionValues transitionValues = values.itemIdValues.valueAt(i); + if (isValidTarget(transitionValues.view, id)) { + included.itemIdValues.put(id, transitionValues); + } + } + return included; + } + /** * @hide */ @@ -315,23 +350,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 +409,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 +445,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..6e6496c 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 @@ -100,14 +109,14 @@ public abstract class Visibility extends Transition { final VisibilityInfo visInfo = new VisibilityInfo(); visInfo.visibilityChange = false; visInfo.fadeIn = false; - if (startValues != null) { + if (startValues != null && startValues.values.containsKey(PROPNAME_VISIBILITY)) { visInfo.startVisibility = (Integer) startValues.values.get(PROPNAME_VISIBILITY); visInfo.startParent = (ViewGroup) startValues.values.get(PROPNAME_PARENT); } else { visInfo.startVisibility = -1; visInfo.startParent = null; } - if (endValues != null) { + if (endValues != null && endValues.values.containsKey(PROPNAME_VISIBILITY)) { visInfo.endVisibility = (Integer) endValues.values.get(PROPNAME_VISIBILITY); visInfo.endParent = (ViewGroup) endValues.values.get(PROPNAME_PARENT); } else { @@ -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,145 @@ 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) { + int originalVisibility = viewToKeep.getVisibility(); + viewToKeep.setVisibility(View.VISIBLE); + Animator animator = onDisappear(sceneRoot, viewToKeep, startValues, endValues); + if (animator == null) { + viewToKeep.setVisibility(originalVisibility); + } 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..538f8a1 --- /dev/null +++ b/core/java/android/tv/ITvInputClient.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.content.ComponentName; +import android.tv.ITvInputSession; +import android.view.InputChannel; + +/** + * 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, in InputChannel channel, 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..a4c99e4 --- /dev/null +++ b/core/java/android/tv/ITvInputManager.aidl @@ -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.tv; + +import android.content.ComponentName; +import android.graphics.Rect; +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); + + void createOverlayView(in IBinder sessionToken, in IBinder windowToken, in Rect frame, + int userId); + void relayoutOverlayView(in IBinder sessionToken, in Rect frame, int userId); + void removeOverlayView(in IBinder sessionToken, int userId); +} diff --git a/core/java/android/tv/ITvInputService.aidl b/core/java/android/tv/ITvInputService.aidl new file mode 100644 index 0000000..4f1bc2b --- /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.ITvInputSessionCallback; +import android.view.InputChannel; + +/** + * 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(in InputChannel channel, 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..32fee4b --- /dev/null +++ b/core/java/android/tv/ITvInputSession.aidl @@ -0,0 +1,39 @@ +/* + * 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.graphics.Rect; +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); + + void createOverlayView(in IBinder windowToken, in Rect frame); + void relayoutOverlayView(in Rect frame); + void removeOverlayView(); +} 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..3ccccf3 --- /dev/null +++ b/core/java/android/tv/ITvInputSessionWrapper.java @@ -0,0 +1,179 @@ +/* + * 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.graphics.Rect; +import android.net.Uri; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.tv.TvInputManager.Session; +import android.tv.TvInputService.TvInputSessionImpl; +import android.util.Log; +import android.view.InputChannel; +import android.view.InputEvent; +import android.view.InputEventReceiver; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.Surface; + +import com.android.internal.os.HandlerCaller; +import com.android.internal.os.SomeArgs; + +/** + * 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 static final int DO_CREATE_OVERLAY_VIEW = 5; + private static final int DO_RELAYOUT_OVERLAY_VIEW = 6; + private static final int DO_REMOVE_OVERLAY_VIEW = 7; + + private final HandlerCaller mCaller; + + private TvInputSessionImpl mTvInputSessionImpl; + private InputChannel mChannel; + private TvInputEventReceiver mReceiver; + + public ITvInputSessionWrapper(Context context, TvInputSessionImpl sessionImpl, + InputChannel channel) { + mCaller = new HandlerCaller(context, null, this, true /* asyncHandler */); + mTvInputSessionImpl = sessionImpl; + mChannel = channel; + if (channel != null) { + mReceiver = new TvInputEventReceiver(channel, context.getMainLooper()); + } + } + + @Override + public void executeMessage(Message msg) { + if (mTvInputSessionImpl == null) { + return; + } + + switch (msg.what) { + case DO_RELEASE: { + mTvInputSessionImpl.release(); + mTvInputSessionImpl = null; + if (mReceiver != null) { + mReceiver.dispose(); + mReceiver = null; + } + if (mChannel != null) { + mChannel.dispose(); + mChannel = null; + } + return; + } + case DO_SET_SURFACE: { + mTvInputSessionImpl.setSurface((Surface) msg.obj); + return; + } + case DO_SET_VOLUME: { + mTvInputSessionImpl.setVolume((Float) msg.obj); + return; + } + case DO_TUNE: { + mTvInputSessionImpl.tune((Uri) msg.obj); + return; + } + case DO_CREATE_OVERLAY_VIEW: { + SomeArgs args = (SomeArgs) msg.obj; + mTvInputSessionImpl.createOverlayView((IBinder) args.arg1, (Rect) args.arg2); + args.recycle(); + return; + } + case DO_RELAYOUT_OVERLAY_VIEW: { + mTvInputSessionImpl.relayoutOverlayView((Rect) msg.obj); + return; + } + case DO_REMOVE_OVERLAY_VIEW: { + mTvInputSessionImpl.removeOverlayView(true); + 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)); + } + + @Override + public void createOverlayView(IBinder windowToken, Rect frame) { + mCaller.executeOrSendMessage(mCaller.obtainMessageOO(DO_CREATE_OVERLAY_VIEW, windowToken, + frame)); + } + + @Override + public void relayoutOverlayView(Rect frame) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_RELAYOUT_OVERLAY_VIEW, frame)); + } + + @Override + public void removeOverlayView() { + mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_REMOVE_OVERLAY_VIEW)); + } + + private final class TvInputEventReceiver extends InputEventReceiver { + public TvInputEventReceiver(InputChannel inputChannel, Looper looper) { + super(inputChannel, looper); + } + + @Override + public void onInputEvent(InputEvent event) { + if (mTvInputSessionImpl == null) { + // The session has been finished. + finishInputEvent(event, false); + return; + } + + int handled = mTvInputSessionImpl.dispatchInputEvent(event, this); + if (handled != Session.DISPATCH_IN_PROGRESS) { + finishInputEvent(event, handled == Session.DISPATCH_HANDLED); + } + } + } +} 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..7b9b1fb --- /dev/null +++ b/core/java/android/tv/TvInputManager.java @@ -0,0 +1,742 @@ +/* + * 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.graphics.Rect; +import android.net.Uri; +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.Pools.Pool; +import android.util.Pools.SimplePool; +import android.util.SparseArray; +import android.view.InputChannel; +import android.view.InputEvent; +import android.view.InputEventSender; +import android.view.Surface; +import android.view.View; + +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, InputChannel channel, + 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(token, channel, 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 { + static final int DISPATCH_IN_PROGRESS = -1; + static final int DISPATCH_NOT_HANDLED = 0; + static final int DISPATCH_HANDLED = 1; + + private static final long INPUT_SESSION_NOT_RESPONDING_TIMEOUT = 2500; + + private final ITvInputManager mService; + private final int mUserId; + + // For scheduling input event handling on the main thread. This also serves as a lock to + // protect pending input events and the input channel. + private final InputEventHandler mHandler = new InputEventHandler(Looper.getMainLooper()); + + private final Pool<PendingEvent> mPendingEventPool = new SimplePool<PendingEvent>(20); + private final SparseArray<PendingEvent> mPendingEvents = new SparseArray<PendingEvent>(20); + + private IBinder mToken; + private TvInputEventSender mSender; + private InputChannel mChannel; + + /** @hide */ + private Session(IBinder token, InputChannel channel, ITvInputManager service, int userId) { + mToken = token; + mChannel = channel; + mService = service; + mUserId = userId; + } + + /** + * Releases this session. + * + * @throws IllegalStateException if the session has been already released. + */ + public void release() { + if (mToken == null) { + throw new IllegalStateException("the session has been already released"); + } + try { + mService.releaseSession(mToken, mUserId); + mToken = null; + } catch (RemoteException e) { + throw new RuntimeException(e); + } + + synchronized (mHandler) { + if (mChannel != null) { + if (mSender != null) { + flushPendingEventsLocked(); + mSender.dispose(); + mSender = null; + } + mChannel.dispose(); + mChannel = null; + } + } + } + + /** + * Sets the {@link android.view.Surface} for this session. + * + * @param surface A {@link android.view.Surface} used to render video. + * @throws IllegalStateException if the session has been already released. + */ + void setSurface(Surface surface) { + if (mToken == null) { + throw new IllegalStateException("the session has been already released"); + } + // 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. + * @throws IllegalStateException if the session has been already released. + */ + public void setVolume(float volume) { + if (mToken == null) { + throw new IllegalStateException("the session has been already released"); + } + 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}. + * @throws IllegalStateException if the session has been already released. + */ + public void tune(Uri channelUri) { + if (channelUri == null) { + throw new IllegalArgumentException("channelUri cannot be null"); + } + if (mToken == null) { + throw new IllegalStateException("the session has been already released"); + } + try { + mService.tune(mToken, channelUri, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** + * Creates an overlay view. Once the overlay view is created, {@link #relayoutOverlayView} + * should be called whenever the layout of its containing view is changed. + * {@link #removeOverlayView()} should be called to remove the overlay view. + * Since a session can have only one overlay view, this method should be called only once + * or it can be called again after calling {@link #removeOverlayView()}. + * + * @param view A view playing TV. + * @param frame A position of the overlay view. + * @throws IllegalArgumentException if any of the arguments is {@code null}. + * @throws IllegalStateException if {@code view} is not attached to a window or + * if the session has been already released. + */ + void createOverlayView(View view, Rect frame) { + if (view == null) { + throw new IllegalArgumentException("view cannot be null"); + } + if (frame == null) { + throw new IllegalArgumentException("frame cannot be null"); + } + if (view.getWindowToken() == null) { + throw new IllegalStateException("view must be attached to a window"); + } + if (mToken == null) { + throw new IllegalStateException("the session has been already released"); + } + try { + mService.createOverlayView(mToken, view.getWindowToken(), frame, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** + * Relayouts the current overlay view. + * + * @param frame A new position of the overlay view. + * @throws IllegalArgumentException if the arguments is {@code null}. + * @throws IllegalStateException if the session has been already released. + */ + void relayoutOverlayView(Rect frame) { + if (frame == null) { + throw new IllegalArgumentException("frame cannot be null"); + } + if (mToken == null) { + throw new IllegalStateException("the session has been already released"); + } + try { + mService.relayoutOverlayView(mToken, frame, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** + * Removes the current overlay view. + * + * @throws IllegalStateException if the session has been already released. + */ + void removeOverlayView() { + if (mToken == null) { + throw new IllegalStateException("the session has been already released"); + } + try { + mService.removeOverlayView(mToken, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** + * Dispatches an input event to this session. + * + * @param event {@link InputEvent} to dispatch. + * @param token A token used to identify the input event later in the callback. + * @param callback A callback used to receive the dispatch result. + * @param handler {@link Handler} that the dispatch result will be delivered to. + * @return Returns {@link #DISPATCH_HANDLED} if the event was handled. Returns + * {@link #DISPATCH_NOT_HANDLED} if the event was not handled. Returns + * {@link #DISPATCH_IN_PROGRESS} if the event is in progress and the callback will + * be invoked later. + * @throws IllegalArgumentException if any of the necessary arguments is {@code null}. + * @hide + */ + public int dispatchInputEvent(InputEvent event, Object token, + FinishedInputEventCallback callback, Handler handler) { + if (event == null) { + throw new IllegalArgumentException("event cannot be null"); + } + if (callback != null && handler == null) { + throw new IllegalArgumentException("handler cannot be null"); + } + synchronized (mHandler) { + if (mChannel == null) { + return DISPATCH_NOT_HANDLED; + } + PendingEvent p = obtainPendingEventLocked(event, token, callback, handler); + if (Looper.myLooper() == Looper.getMainLooper()) { + // Already running on the main thread so we can send the event immediately. + return sendInputEventOnMainLooperLocked(p); + } + + // Post the event to the main thread. + Message msg = mHandler.obtainMessage(InputEventHandler.MSG_SEND_INPUT_EVENT, p); + msg.setAsynchronous(true); + mHandler.sendMessage(msg); + return DISPATCH_IN_PROGRESS; + } + } + + /** + * Callback that is invoked when an input event that was dispatched to this session has been + * finished. + * + * @hide + */ + public interface FinishedInputEventCallback { + /** + * Called when the dispatched input event is finished. + * + * @param token a token passed to {@link #dispatchInputEvent}. + * @param handled {@code true} if the dispatched input event was handled properly. + * {@code false} otherwise. + */ + public void onFinishedInputEvent(Object token, boolean handled); + } + + // Must be called on the main looper + private void sendInputEventAndReportResultOnMainLooper(PendingEvent p) { + synchronized (mHandler) { + int result = sendInputEventOnMainLooperLocked(p); + if (result == DISPATCH_IN_PROGRESS) { + return; + } + } + + invokeFinishedInputEventCallback(p, false); + } + + private int sendInputEventOnMainLooperLocked(PendingEvent p) { + if (mChannel != null) { + if (mSender == null) { + mSender = new TvInputEventSender(mChannel, mHandler.getLooper()); + } + + final InputEvent event = p.mEvent; + final int seq = event.getSequenceNumber(); + if (mSender.sendInputEvent(seq, event)) { + mPendingEvents.put(seq, p); + Message msg = mHandler.obtainMessage(InputEventHandler.MSG_TIMEOUT_INPUT_EVENT, p); + msg.setAsynchronous(true); + mHandler.sendMessageDelayed(msg, INPUT_SESSION_NOT_RESPONDING_TIMEOUT); + return DISPATCH_IN_PROGRESS; + } + + Log.w(TAG, "Unable to send input event to session: " + mToken + " dropping:" + + event); + } + return DISPATCH_NOT_HANDLED; + } + + void finishedInputEvent(int seq, boolean handled, boolean timeout) { + final PendingEvent p; + synchronized (mHandler) { + int index = mPendingEvents.indexOfKey(seq); + if (index < 0) { + return; // spurious, event already finished or timed out + } + + p = mPendingEvents.valueAt(index); + mPendingEvents.removeAt(index); + + if (timeout) { + Log.w(TAG, "Timeout waiting for seesion to handle input event after " + + INPUT_SESSION_NOT_RESPONDING_TIMEOUT + " ms: " + mToken); + } else { + mHandler.removeMessages(InputEventHandler.MSG_TIMEOUT_INPUT_EVENT, p); + } + } + + invokeFinishedInputEventCallback(p, handled); + } + + // Assumes the event has already been removed from the queue. + void invokeFinishedInputEventCallback(PendingEvent p, boolean handled) { + p.mHandled = handled; + if (p.mHandler.getLooper().isCurrentThread()) { + // Already running on the callback handler thread so we can send the callback + // immediately. + p.run(); + } else { + // Post the event to the callback handler thread. + // In this case, the callback will be responsible for recycling the event. + Message msg = Message.obtain(p.mHandler, p); + msg.setAsynchronous(true); + msg.sendToTarget(); + } + } + + private void flushPendingEventsLocked() { + mHandler.removeMessages(InputEventHandler.MSG_FLUSH_INPUT_EVENT); + + final int count = mPendingEvents.size(); + for (int i = 0; i < count; i++) { + int seq = mPendingEvents.keyAt(i); + Message msg = mHandler.obtainMessage(InputEventHandler.MSG_FLUSH_INPUT_EVENT, seq, 0); + msg.setAsynchronous(true); + msg.sendToTarget(); + } + } + + private PendingEvent obtainPendingEventLocked(InputEvent event, Object token, + FinishedInputEventCallback callback, Handler handler) { + PendingEvent p = mPendingEventPool.acquire(); + if (p == null) { + p = new PendingEvent(); + } + p.mEvent = event; + p.mToken = token; + p.mCallback = callback; + p.mHandler = handler; + return p; + } + + private void recyclePendingEventLocked(PendingEvent p) { + p.recycle(); + mPendingEventPool.release(p); + } + + private final class InputEventHandler extends Handler { + public static final int MSG_SEND_INPUT_EVENT = 1; + public static final int MSG_TIMEOUT_INPUT_EVENT = 2; + public static final int MSG_FLUSH_INPUT_EVENT = 3; + + InputEventHandler(Looper looper) { + super(looper, null, true); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_SEND_INPUT_EVENT: { + sendInputEventAndReportResultOnMainLooper((PendingEvent) msg.obj); + return; + } + case MSG_TIMEOUT_INPUT_EVENT: { + finishedInputEvent(msg.arg1, false, true); + return; + } + case MSG_FLUSH_INPUT_EVENT: { + finishedInputEvent(msg.arg1, false, false); + return; + } + } + } + } + + private final class TvInputEventSender extends InputEventSender { + public TvInputEventSender(InputChannel inputChannel, Looper looper) { + super(inputChannel, looper); + } + + @Override + public void onInputEventFinished(int seq, boolean handled) { + finishedInputEvent(seq, handled, false); + } + } + + private final class PendingEvent implements Runnable { + public InputEvent mEvent; + public Object mToken; + public FinishedInputEventCallback mCallback; + public Handler mHandler; + public boolean mHandled; + + public void recycle() { + mEvent = null; + mToken = null; + mCallback = null; + mHandler = null; + mHandled = false; + } + + @Override + public void run() { + mCallback.onFinishedInputEvent(mToken, mHandled); + + synchronized (mHandler) { + recyclePendingEventLocked(this); + } + } + } + } +} diff --git a/core/java/android/tv/TvInputService.java b/core/java/android/tv/TvInputService.java new file mode 100644 index 0000000..70e7f95 --- /dev/null +++ b/core/java/android/tv/TvInputService.java @@ -0,0 +1,551 @@ +/* + * 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.Context; +import android.content.Intent; +import android.graphics.PixelFormat; +import android.graphics.Rect; +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.tv.TvInputManager.Session; +import android.util.Log; +import android.view.Gravity; +import android.view.InputChannel; +import android.view.InputDevice; +import android.view.InputEvent; +import android.view.InputEventReceiver; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.Surface; +import android.view.View; +import android.view.WindowManager; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.os.SomeArgs; + +/** + * 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(InputChannel channel, ITvInputSessionCallback cb) { + if (channel == null) { + Log.w(TAG, "Creating session without input channel"); + } + if (cb == null) { + return; + } + SomeArgs args = SomeArgs.obtain(); + args.arg1 = channel; + args.arg2 = cb; + mHandler.obtainMessage(ServiceHandler.DO_CREATE_SESSION, args).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 class TvInputSessionImpl implements KeyEvent.Callback { + private final KeyEvent.DispatcherState mDispatcherState = new KeyEvent.DispatcherState(); + private final WindowManager mWindowManager; + private WindowManager.LayoutParams mWindowParams; + private Surface mSurface; + private View mOverlayView; + private boolean mOverlayViewEnabled; + private IBinder mWindowToken; + private Rect mOverlayFrame; + + public TvInputSessionImpl() { + mWindowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE); + } + + /** + * Enables or disables the overlay view. By default, the overlay view is disabled. Must be + * called explicitly after the session is created to enable the overlay view. + * + * @param enable {@code true} if you want to enable the overlay view. {@code false} + * otherwise. + */ + public void setOverlayViewEnabled(final boolean enable) { + mHandler.post(new Runnable() { + @Override + public void run() { + if (enable == mOverlayViewEnabled) { + return; + } + mOverlayViewEnabled = enable; + if (enable) { + if (mWindowToken != null) { + createOverlayView(mWindowToken, mOverlayFrame); + } + } else { + removeOverlayView(false); + } + } + }); + } + + /** + * 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); + + /** + * Called when an application requests to create an overlay view. Each session + * implementation can override this method and return its own view. + * + * @return a view attached to the overlay window + */ + public View onCreateOverlayView() { + return null; + } + + /** + * Default implementation of {@link android.view.KeyEvent.Callback#onKeyDown(int, KeyEvent) + * KeyEvent.Callback.onKeyDown()}: always returns false (doesn't handle the event). + * <p> + * Override this to intercept key down events before they are processed by the application. + * If you return true, the application will not process the event itself. If you return + * false, the normal application processing will occur as if the TV input had not seen the + * event at all. + * + * @param keyCode The value in event.getKeyCode(). + * @param event Description of the key event. + * @return If you handled the event, return {@code true}. If you want to allow the event to + * be handled by the next receiver, return {@code false}. + */ + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return false; + } + + /** + * Default implementation of + * {@link android.view.KeyEvent.Callback#onKeyLongPress(int, KeyEvent) + * KeyEvent.Callback.onKeyLongPress()}: always returns false (doesn't handle the event). + * <p> + * Override this to intercept key long press events before they are processed by the + * application. If you return true, the application will not process the event itself. If + * you return false, the normal application processing will occur as if the TV input had not + * seen the event at all. + * + * @param keyCode The value in event.getKeyCode(). + * @param event Description of the key event. + * @return If you handled the event, return {@code true}. If you want to allow the event to + * be handled by the next receiver, return {@code false}. + */ + @Override + public boolean onKeyLongPress(int keyCode, KeyEvent event) { + return false; + } + + /** + * Default implementation of + * {@link android.view.KeyEvent.Callback#onKeyMultiple(int, int, KeyEvent) + * KeyEvent.Callback.onKeyMultiple()}: always returns false (doesn't handle the event). + * <p> + * Override this to intercept special key multiple events before they are processed by the + * application. If you return true, the application will not itself process the event. If + * you return false, the normal application processing will occur as if the TV input had not + * seen the event at all. + * + * @param keyCode The value in event.getKeyCode(). + * @param count The number of times the action was made. + * @param event Description of the key event. + * @return If you handled the event, return {@code true}. If you want to allow the event to + * be handled by the next receiver, return {@code false}. + */ + @Override + public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) { + return false; + } + + /** + * Default implementation of {@link android.view.KeyEvent.Callback#onKeyUp(int, KeyEvent) + * KeyEvent.Callback.onKeyUp()}: always returns false (doesn't handle the event). + * <p> + * Override this to intercept key up events before they are processed by the application. If + * you return true, the application will not itself process the event. If you return false, + * the normal application processing will occur as if the TV input had not seen the event at + * all. + * + * @param keyCode The value in event.getKeyCode(). + * @param event Description of the key event. + * @return If you handled the event, return {@code true}. If you want to allow the event to + * be handled by the next receiver, return {@code false}. + */ + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + return false; + } + + /** + * Implement this method to handle touch screen motion events on the current input session. + * + * @param event The motion event being received. + * @return If you handled the event, return {@code true}. If you want to allow the event to + * be handled by the next receiver, return {@code false}. + * @see View#onTouchEvent + */ + public boolean onTouchEvent(MotionEvent event) { + return false; + } + + /** + * Implement this method to handle trackball events on the current input session. + * + * @param event The motion event being received. + * @return If you handled the event, return {@code true}. If you want to allow the event to + * be handled by the next receiver, return {@code false}. + * @see View#onTrackballEvent + */ + public boolean onTrackballEvent(MotionEvent event) { + return false; + } + + /** + * Implement this method to handle generic motion events on the current input session. + * + * @param event The motion event being received. + * @return If you handled the event, return {@code true}. If you want to allow the event to + * be handled by the next receiver, return {@code false}. + * @see View#onGenericMotionEvent + */ + public boolean onGenericMotionEvent(MotionEvent event) { + return false; + } + + /** + * This method is called when the application would like to stop using the current input + * session. + */ + void release() { + onRelease(); + if (mSurface != null) { + mSurface.release(); + mSurface = null; + } + removeOverlayView(true); + } + + /** + * Calls {@link #onSetSurface}. + */ + void setSurface(Surface surface) { + onSetSurface(surface); + if (mSurface != null) { + mSurface.release(); + } + mSurface = 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. + } + + /** + * Creates an overlay view. This calls {@link #onCreateOverlayView} to get a view to attach + * to the overlay window. + * + * @param windowToken A window token of an application. + * @param frame A position of the overlay view. + */ + void createOverlayView(IBinder windowToken, Rect frame) { + if (mOverlayView != null) { + mWindowManager.removeView(mOverlayView); + mOverlayView = null; + } + if (DEBUG) { + Log.d(TAG, "create overlay view(" + frame + ")"); + } + mWindowToken = windowToken; + mOverlayFrame = frame; + if (!mOverlayViewEnabled) { + return; + } + mOverlayView = onCreateOverlayView(); + if (mOverlayView == null) { + return; + } + // TvView's window type is TYPE_APPLICATION_MEDIA and we want to create + // an overlay window above the media window but below the application window. + int type = WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA_OVERLAY; + // We make the overlay view non-focusable and non-touchable so that + // the application that owns the window token can decide whether to consume or + // dispatch the input events. + int flag = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; + mWindowParams = new WindowManager.LayoutParams( + frame.right - frame.left, frame.bottom - frame.top, + frame.left, frame.top, type, flag, PixelFormat.TRANSPARENT); + mWindowParams.privateFlags |= + WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; + mWindowParams.gravity = Gravity.START | Gravity.TOP; + mWindowParams.token = windowToken; + mWindowManager.addView(mOverlayView, mWindowParams); + } + + /** + * Relayouts the current overlay view. + * + * @param frame A new position of the overlay view. + */ + void relayoutOverlayView(Rect frame) { + if (DEBUG) { + Log.d(TAG, "relayout overlay view(" + frame + ")"); + } + mOverlayFrame = frame; + if (!mOverlayViewEnabled || mOverlayView == null) { + return; + } + mWindowParams.x = frame.left; + mWindowParams.y = frame.top; + mWindowParams.width = frame.right - frame.left; + mWindowParams.height = frame.bottom - frame.top; + mWindowManager.updateViewLayout(mOverlayView, mWindowParams); + } + + /** + * Removes the current overlay view. + */ + void removeOverlayView(boolean clearWindowToken) { + if (DEBUG) { + Log.d(TAG, "remove overlay view(" + mOverlayView + ")"); + } + if (clearWindowToken) { + mWindowToken = null; + mOverlayFrame = null; + } + if (mOverlayView != null) { + mWindowManager.removeView(mOverlayView); + mOverlayView = null; + mWindowParams = null; + } + } + + /** + * Takes care of dispatching incoming input events and tells whether the event was handled. + */ + int dispatchInputEvent(InputEvent event, InputEventReceiver receiver) { + if (DEBUG) Log.d(TAG, "dispatchInputEvent(" + event + ")"); + if (event instanceof KeyEvent) { + if (((KeyEvent) event).dispatch(this, mDispatcherState, this)) { + return Session.DISPATCH_HANDLED; + } + } else if (event instanceof MotionEvent) { + MotionEvent motionEvent = (MotionEvent) event; + final int source = motionEvent.getSource(); + if (motionEvent.isTouchEvent()) { + if (onTouchEvent(motionEvent)) { + return Session.DISPATCH_HANDLED; + } + } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) { + if (onTrackballEvent(motionEvent)) { + return Session.DISPATCH_HANDLED; + } + } else { + if (onGenericMotionEvent(motionEvent)) { + return Session.DISPATCH_HANDLED; + } + } + } + if (mOverlayView == null) { + return Session.DISPATCH_NOT_HANDLED; + } + if (!mOverlayView.hasWindowFocus()) { + mOverlayView.getViewRootImpl().windowFocusChanged(true, true); + } + mOverlayView.getViewRootImpl().dispatchInputEvent(event, receiver); + return Session.DISPATCH_IN_PROGRESS; + } + } + + 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: { + SomeArgs args = (SomeArgs) msg.obj; + InputChannel channel = (InputChannel) args.arg1; + ITvInputSessionCallback cb = (ITvInputSessionCallback) args.arg2; + try { + TvInputSessionImpl sessionImpl = onCreateSession(); + if (sessionImpl == null) { + // Failed to create a session. + cb.onSessionCreated(null); + } else { + ITvInputSession stub = new ITvInputSessionWrapper(TvInputService.this, + sessionImpl, channel); + cb.onSessionCreated(stub); + } + } catch (RemoteException e) { + Log.e(TAG, "error in onSessionCreated"); + } + args.recycle(); + 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/tv/TvView.java b/core/java/android/tv/TvView.java new file mode 100644 index 0000000..289823b --- /dev/null +++ b/core/java/android/tv/TvView.java @@ -0,0 +1,360 @@ +/* + * 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.Context; +import android.graphics.Rect; +import android.os.Handler; +import android.tv.TvInputManager.Session; +import android.tv.TvInputManager.Session.FinishedInputEventCallback; +import android.tv.TvInputManager.SessionCreateCallback; +import android.util.AttributeSet; +import android.util.Log; +import android.view.InputEvent; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; + +/** + * View playing TV + */ +public class TvView extends SurfaceView { + // STOPSHIP: Turn debugging off. + private static final boolean DEBUG = true; + private static final String TAG = "TvView"; + + private final Handler mHandler = new Handler(); + private TvInputManager.Session mSession; + private Surface mSurface; + private boolean mOverlayViewCreated; + private Rect mOverlayViewFrame; + private final TvInputManager mTvInputManager; + private SessionCreateCallback mSessionCreateCallback; + private OnUnhandledInputEventListener mOnUnhandledInputEventListener; + + private final SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() { + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + Log.d(TAG, "surfaceChanged(holder=" + holder + ", format=" + format + ", width=" + width + + ", height=" + height + ")"); + if (holder.getSurface() == mSurface) { + return; + } + mSurface = holder.getSurface(); + setSessionSurface(mSurface); + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + mSurface = holder.getSurface(); + setSessionSurface(mSurface); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + mSurface = null; + setSessionSurface(null); + } + }; + + private final FinishedInputEventCallback mFinishedInputEventCallback = + new FinishedInputEventCallback() { + @Override + public void onFinishedInputEvent(Object token, boolean handled) { + if (DEBUG) { + Log.d(TAG, "onFinishedInputEvent(token=" + token + ", handled=" + handled + ")"); + } + if (handled) { + return; + } + // TODO: Re-order unhandled events. + InputEvent event = (InputEvent) token; + if (dispatchUnhandledInputEvent(event)) { + return; + } + getViewRootImpl().dispatchUnhandledInputEvent(event); + } + }; + + public TvView(Context context) { + this(context, null, 0); + } + + public TvView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public TvView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + getHolder().addCallback(mSurfaceHolderCallback); + mTvInputManager = (TvInputManager) getContext().getSystemService(Context.TV_INPUT_SERVICE); + } + + /** + * Binds a TV input to this view. {@link SessionCreateCallback#onSessionCreated} will be + * called to send the result of this binding with {@link TvInputManager.Session}. + * If a TV input is already bound, the input will be unbound from this view and its session + * will be released. + * + * @param name TV input name will be bound to this view. + * @param callback called when TV input is bound. The callback sends + * {@link TvInputManager.Session} + * @throws IllegalArgumentException if any of the arguments is {@code null}. + */ + public void bindTvInput(ComponentName name, SessionCreateCallback callback) { + if (name == null) { + throw new IllegalArgumentException("name cannot be null"); + } + if (callback == null) { + throw new IllegalArgumentException("callback cannot be null"); + } + if (mSession != null) { + release(); + } + // When bindTvInput is called multiple times before the callback is called, + // only the callback of the last bindTvInput call will be actually called back. + // The previous callbacks will be ignored. For the logic, mSessionCreateCallback + // is newly assigned for every bindTvInput call and compared with + // MySessionCreateCallback.this. + mSessionCreateCallback = new MySessionCreateCallback(callback); + mTvInputManager.createSession(name, mSessionCreateCallback, mHandler); + } + + /** + * Unbinds a TV input currently bound. Its corresponding {@link TvInputManager.Session} + * is released. + */ + public void unbindTvInput() { + if (mSession != null) { + release(); + } + } + + /** + * Dispatches an unhandled input event to the next receiver. + * <p> + * Except system keys, TvView always consumes input events in the normal flow. This is called + * asynchronously from where the event is dispatched. It gives the host application a chance to + * dispatch the unhandled input events. + * + * @param event The input event. + * @return {@code true} if the event was handled by the view, {@code false} otherwise. + */ + public boolean dispatchUnhandledInputEvent(InputEvent event) { + if (mOnUnhandledInputEventListener != null) { + if (mOnUnhandledInputEventListener.onUnhandledInputEvent(event)) { + return true; + } + } + return onUnhandledInputEvent(event); + } + + /** + * Called when an unhandled input event was also not handled by the user provided callback. This + * is the last chance to handle the unhandled input event in the TvView. + * + * @param event The input event. + * @return If you handled the event, return {@code true}. If you want to allow the event to be + * handled by the next receiver, return {@code false}. + */ + public boolean onUnhandledInputEvent(InputEvent event) { + return false; + } + + /** + * Registers a callback to be invoked when an input event was not handled by the bound TV input. + * + * @param listener The callback to invoke when the unhandled input event was received. + */ + public void setOnUnhandledInputEventListener(OnUnhandledInputEventListener listener) { + mOnUnhandledInputEventListener = listener; + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (super.dispatchKeyEvent(event)) { + return true; + } + if (DEBUG) Log.d(TAG, "dispatchKeyEvent(" + event + ")"); + if (mSession == null) { + return false; + } + int ret = mSession.dispatchInputEvent(event, event, mFinishedInputEventCallback, mHandler); + return ret != Session.DISPATCH_NOT_HANDLED; + } + + @Override + public boolean dispatchTouchEvent(MotionEvent event) { + if (super.dispatchTouchEvent(event)) { + return true; + } + if (DEBUG) Log.d(TAG, "dispatchTouchEvent(" + event + ")"); + if (mSession == null) { + return false; + } + int ret = mSession.dispatchInputEvent(event, event, mFinishedInputEventCallback, mHandler); + return ret != Session.DISPATCH_NOT_HANDLED; + } + + @Override + public boolean dispatchTrackballEvent(MotionEvent event) { + if (super.dispatchTrackballEvent(event)) { + return true; + } + if (DEBUG) Log.d(TAG, "dispatchTrackballEvent(" + event + ")"); + if (mSession == null) { + return false; + } + int ret = mSession.dispatchInputEvent(event, event, mFinishedInputEventCallback, mHandler); + return ret != Session.DISPATCH_NOT_HANDLED; + } + + @Override + public boolean dispatchGenericMotionEvent(MotionEvent event) { + if (super.dispatchGenericMotionEvent(event)) { + return true; + } + if (DEBUG) Log.d(TAG, "dispatchGenericMotionEvent(" + event + ")"); + if (mSession == null) { + return false; + } + int ret = mSession.dispatchInputEvent(event, event, mFinishedInputEventCallback, mHandler); + return ret != Session.DISPATCH_NOT_HANDLED; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + createSessionOverlayView(); + } + + @Override + protected void onDetachedFromWindow() { + removeSessionOverlayView(); + super.onDetachedFromWindow(); + } + + /** @hide */ + @Override + protected void updateWindow(boolean force, boolean redrawNeeded) { + super.updateWindow(force, redrawNeeded); + relayoutSessionOverlayView(); + } + + private void release() { + setSessionSurface(null); + removeSessionOverlayView(); + mSession.release(); + mSession = null; + } + + private void setSessionSurface(Surface surface) { + if (mSession == null) { + return; + } + mSession.setSurface(surface); + } + + private void createSessionOverlayView() { + if (mSession == null || !isAttachedToWindow() + || mOverlayViewCreated) { + return; + } + mOverlayViewFrame = getViewFrameOnScreen(); + mSession.createOverlayView(this, mOverlayViewFrame); + mOverlayViewCreated = true; + } + + private void removeSessionOverlayView() { + if (mSession == null || !mOverlayViewCreated) { + return; + } + mSession.removeOverlayView(); + mOverlayViewCreated = false; + mOverlayViewFrame = null; + } + + private void relayoutSessionOverlayView() { + if (mSession == null || !isAttachedToWindow() + || !mOverlayViewCreated) { + return; + } + Rect viewFrame = getViewFrameOnScreen(); + if (viewFrame.equals(mOverlayViewFrame)) { + return; + } + mSession.relayoutOverlayView(viewFrame); + mOverlayViewFrame = viewFrame; + } + + private Rect getViewFrameOnScreen() { + int[] location = new int[2]; + getLocationOnScreen(location); + return new Rect(location[0], location[1], + location[0] + getWidth(), location[1] + getHeight()); + } + + /** + * Interface definition for a callback to be invoked when the unhandled input event is received. + */ + public interface OnUnhandledInputEventListener { + /** + * Called when an input event was not handled by the bound TV input. + * <p> + * This is called asynchronously from where the event is dispatched. It gives the host + * application a chance to handle the unhandled input events. + * + * @param event The input event. + * @return If you handled the event, return {@code true}. If you want to allow the event to + * be handled by the next receiver, return {@code false}. + */ + boolean onUnhandledInputEvent(InputEvent event); + } + + private class MySessionCreateCallback implements SessionCreateCallback { + final SessionCreateCallback mExternalCallback; + + MySessionCreateCallback(SessionCreateCallback externalCallback) { + mExternalCallback = externalCallback; + } + + @Override + public void onSessionCreated(Session session) { + if (this != mSessionCreateCallback) { + // This callback is obsolete. + session.release(); + return; + } + mSession = session; + if (session != null) { + // mSurface may not be ready yet as soon as starting an application. + // In the case, we don't send Session.setSurface(null) unnecessarily. + // setSessionSurface will be called in surfaceCreated. + if (mSurface != null) { + setSessionSurface(mSurface); + } + createSessionOverlayView(); + } + if (mExternalCallback != null) { + mExternalCallback.onSessionCreated(session); + } + } + } +} 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..2cc91b9 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,27 @@ 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 GOOD_GTLD_CHAR = + "a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF"; + private static final String GTLD = "[" + GOOD_GTLD_CHAR + "]{2,63}"; + private static final String HOST_NAME = "(" + IRI + "\\.)+" + GTLD; + + 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 +147,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 +155,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 +171,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..ba1c4b6 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; } /** @@ -100,7 +96,7 @@ public class ContextThemeWrapper extends ContextWrapper { return mTheme; } - mThemeResource = Resources.selectDefaultTheme(mThemeResource, + mThemeResource = getResources().selectDefaultTheme(mThemeResource, getApplicationInfo().targetSdkVersion); initializeTheme(); @@ -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..34b85d9 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); - - @Override - void outputDisplayList(DisplayList displayList) { - nOutputDisplayList(mRenderer, ((GLES20DisplayList) displayList).getNativeDisplayList()); - } - - private static native void nOutputDisplayList(long renderer, long displayList); + protected static native long nFinishRecording(long renderer); @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,24 +373,11 @@ 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); - void interrupt() { - nInterrupt(mRenderer); - } - - void resume() { - nResume(mRenderer); - } - - private static native void nInterrupt(long renderer); - private static native void nResume(long renderer); - /////////////////////////////////////////////////////////////////////////// // Support /////////////////////////////////////////////////////////////////////////// @@ -648,15 +578,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 +588,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 +671,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 +694,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 +704,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 +827,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 +877,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 +917,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 +935,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 +955,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 +973,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 +1040,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 +1090,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 { @@ -1216,11 +1117,11 @@ 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); + public void drawRoundRect(float left, float top, float right, float bottom, float rx, float ry, + Paint paint) { + int modifiers = setupModifiers(paint, MODIFIER_SHADER); try { - nDrawRoundRect(mRenderer, rect.left, rect.top, rect.right, rect.bottom, - rx, ry, paint.mNativePaint); + nDrawRoundRect(mRenderer, left, top, right, bottom, rx, ry, paint.mNativePaint); } finally { if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); } @@ -1397,12 +1298,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 +1319,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 +1337,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..97339cc --- /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 + 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(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(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..d31c79d 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); + 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..05e202b 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; @@ -646,12 +645,11 @@ public class KeyEvent extends InputEvent implements Parcelable { // NOTE: If you add a new keycode here you must also add it to: // isSystem() // frameworks/native/include/android/keycodes.h - // frameworks/base/include/androidfw/KeycodeLabels.h + // frameworks/base/include/androidfw/InputEventAttributes.h // external/webkit/WebKit/android/plugins/ANPKeyCodes.h // frameworks/base/core/res/res/values/attrs.xml // emulator? // LAST_KEYCODE - // KEYCODE_SYMBOLIC_NAMES // // Also Android currently does not reserve code ranges for vendor- // specific key codes. If you have new key codes to have, you @@ -659,237 +657,6 @@ public class KeyEvent extends InputEvent implements Parcelable { // those new codes. This is intended to maintain a consistent // set of key code definitions across all Android devices. - // Symbolic names of all key codes. - private static final SparseArray<String> KEYCODE_SYMBOLIC_NAMES = new SparseArray<String>(); - private static void populateKeycodeSymbolicNames() { - SparseArray<String> names = KEYCODE_SYMBOLIC_NAMES; - names.append(KEYCODE_UNKNOWN, "KEYCODE_UNKNOWN"); - names.append(KEYCODE_SOFT_LEFT, "KEYCODE_SOFT_LEFT"); - names.append(KEYCODE_SOFT_RIGHT, "KEYCODE_SOFT_RIGHT"); - names.append(KEYCODE_HOME, "KEYCODE_HOME"); - names.append(KEYCODE_BACK, "KEYCODE_BACK"); - names.append(KEYCODE_CALL, "KEYCODE_CALL"); - names.append(KEYCODE_ENDCALL, "KEYCODE_ENDCALL"); - names.append(KEYCODE_0, "KEYCODE_0"); - names.append(KEYCODE_1, "KEYCODE_1"); - names.append(KEYCODE_2, "KEYCODE_2"); - names.append(KEYCODE_3, "KEYCODE_3"); - names.append(KEYCODE_4, "KEYCODE_4"); - names.append(KEYCODE_5, "KEYCODE_5"); - names.append(KEYCODE_6, "KEYCODE_6"); - names.append(KEYCODE_7, "KEYCODE_7"); - names.append(KEYCODE_8, "KEYCODE_8"); - names.append(KEYCODE_9, "KEYCODE_9"); - names.append(KEYCODE_STAR, "KEYCODE_STAR"); - names.append(KEYCODE_POUND, "KEYCODE_POUND"); - names.append(KEYCODE_DPAD_UP, "KEYCODE_DPAD_UP"); - names.append(KEYCODE_DPAD_DOWN, "KEYCODE_DPAD_DOWN"); - names.append(KEYCODE_DPAD_LEFT, "KEYCODE_DPAD_LEFT"); - names.append(KEYCODE_DPAD_RIGHT, "KEYCODE_DPAD_RIGHT"); - names.append(KEYCODE_DPAD_CENTER, "KEYCODE_DPAD_CENTER"); - names.append(KEYCODE_VOLUME_UP, "KEYCODE_VOLUME_UP"); - names.append(KEYCODE_VOLUME_DOWN, "KEYCODE_VOLUME_DOWN"); - names.append(KEYCODE_POWER, "KEYCODE_POWER"); - names.append(KEYCODE_CAMERA, "KEYCODE_CAMERA"); - names.append(KEYCODE_CLEAR, "KEYCODE_CLEAR"); - names.append(KEYCODE_A, "KEYCODE_A"); - names.append(KEYCODE_B, "KEYCODE_B"); - names.append(KEYCODE_C, "KEYCODE_C"); - names.append(KEYCODE_D, "KEYCODE_D"); - names.append(KEYCODE_E, "KEYCODE_E"); - names.append(KEYCODE_F, "KEYCODE_F"); - names.append(KEYCODE_G, "KEYCODE_G"); - names.append(KEYCODE_H, "KEYCODE_H"); - names.append(KEYCODE_I, "KEYCODE_I"); - names.append(KEYCODE_J, "KEYCODE_J"); - names.append(KEYCODE_K, "KEYCODE_K"); - names.append(KEYCODE_L, "KEYCODE_L"); - names.append(KEYCODE_M, "KEYCODE_M"); - names.append(KEYCODE_N, "KEYCODE_N"); - names.append(KEYCODE_O, "KEYCODE_O"); - names.append(KEYCODE_P, "KEYCODE_P"); - names.append(KEYCODE_Q, "KEYCODE_Q"); - names.append(KEYCODE_R, "KEYCODE_R"); - names.append(KEYCODE_S, "KEYCODE_S"); - names.append(KEYCODE_T, "KEYCODE_T"); - names.append(KEYCODE_U, "KEYCODE_U"); - names.append(KEYCODE_V, "KEYCODE_V"); - names.append(KEYCODE_W, "KEYCODE_W"); - names.append(KEYCODE_X, "KEYCODE_X"); - names.append(KEYCODE_Y, "KEYCODE_Y"); - names.append(KEYCODE_Z, "KEYCODE_Z"); - names.append(KEYCODE_COMMA, "KEYCODE_COMMA"); - names.append(KEYCODE_PERIOD, "KEYCODE_PERIOD"); - names.append(KEYCODE_ALT_LEFT, "KEYCODE_ALT_LEFT"); - names.append(KEYCODE_ALT_RIGHT, "KEYCODE_ALT_RIGHT"); - names.append(KEYCODE_SHIFT_LEFT, "KEYCODE_SHIFT_LEFT"); - names.append(KEYCODE_SHIFT_RIGHT, "KEYCODE_SHIFT_RIGHT"); - names.append(KEYCODE_TAB, "KEYCODE_TAB"); - names.append(KEYCODE_SPACE, "KEYCODE_SPACE"); - names.append(KEYCODE_SYM, "KEYCODE_SYM"); - names.append(KEYCODE_EXPLORER, "KEYCODE_EXPLORER"); - names.append(KEYCODE_ENVELOPE, "KEYCODE_ENVELOPE"); - names.append(KEYCODE_ENTER, "KEYCODE_ENTER"); - names.append(KEYCODE_DEL, "KEYCODE_DEL"); - names.append(KEYCODE_GRAVE, "KEYCODE_GRAVE"); - names.append(KEYCODE_MINUS, "KEYCODE_MINUS"); - names.append(KEYCODE_EQUALS, "KEYCODE_EQUALS"); - names.append(KEYCODE_LEFT_BRACKET, "KEYCODE_LEFT_BRACKET"); - names.append(KEYCODE_RIGHT_BRACKET, "KEYCODE_RIGHT_BRACKET"); - names.append(KEYCODE_BACKSLASH, "KEYCODE_BACKSLASH"); - names.append(KEYCODE_SEMICOLON, "KEYCODE_SEMICOLON"); - names.append(KEYCODE_APOSTROPHE, "KEYCODE_APOSTROPHE"); - names.append(KEYCODE_SLASH, "KEYCODE_SLASH"); - names.append(KEYCODE_AT, "KEYCODE_AT"); - names.append(KEYCODE_NUM, "KEYCODE_NUM"); - names.append(KEYCODE_HEADSETHOOK, "KEYCODE_HEADSETHOOK"); - names.append(KEYCODE_FOCUS, "KEYCODE_FOCUS"); - names.append(KEYCODE_PLUS, "KEYCODE_PLUS"); - names.append(KEYCODE_MENU, "KEYCODE_MENU"); - names.append(KEYCODE_NOTIFICATION, "KEYCODE_NOTIFICATION"); - names.append(KEYCODE_SEARCH, "KEYCODE_SEARCH"); - names.append(KEYCODE_MEDIA_PLAY_PAUSE, "KEYCODE_MEDIA_PLAY_PAUSE"); - names.append(KEYCODE_MEDIA_STOP, "KEYCODE_MEDIA_STOP"); - names.append(KEYCODE_MEDIA_NEXT, "KEYCODE_MEDIA_NEXT"); - names.append(KEYCODE_MEDIA_PREVIOUS, "KEYCODE_MEDIA_PREVIOUS"); - names.append(KEYCODE_MEDIA_REWIND, "KEYCODE_MEDIA_REWIND"); - names.append(KEYCODE_MEDIA_FAST_FORWARD, "KEYCODE_MEDIA_FAST_FORWARD"); - names.append(KEYCODE_MUTE, "KEYCODE_MUTE"); - names.append(KEYCODE_PAGE_UP, "KEYCODE_PAGE_UP"); - names.append(KEYCODE_PAGE_DOWN, "KEYCODE_PAGE_DOWN"); - names.append(KEYCODE_PICTSYMBOLS, "KEYCODE_PICTSYMBOLS"); - names.append(KEYCODE_SWITCH_CHARSET, "KEYCODE_SWITCH_CHARSET"); - names.append(KEYCODE_BUTTON_A, "KEYCODE_BUTTON_A"); - names.append(KEYCODE_BUTTON_B, "KEYCODE_BUTTON_B"); - names.append(KEYCODE_BUTTON_C, "KEYCODE_BUTTON_C"); - names.append(KEYCODE_BUTTON_X, "KEYCODE_BUTTON_X"); - names.append(KEYCODE_BUTTON_Y, "KEYCODE_BUTTON_Y"); - names.append(KEYCODE_BUTTON_Z, "KEYCODE_BUTTON_Z"); - names.append(KEYCODE_BUTTON_L1, "KEYCODE_BUTTON_L1"); - names.append(KEYCODE_BUTTON_R1, "KEYCODE_BUTTON_R1"); - names.append(KEYCODE_BUTTON_L2, "KEYCODE_BUTTON_L2"); - names.append(KEYCODE_BUTTON_R2, "KEYCODE_BUTTON_R2"); - names.append(KEYCODE_BUTTON_THUMBL, "KEYCODE_BUTTON_THUMBL"); - names.append(KEYCODE_BUTTON_THUMBR, "KEYCODE_BUTTON_THUMBR"); - names.append(KEYCODE_BUTTON_START, "KEYCODE_BUTTON_START"); - names.append(KEYCODE_BUTTON_SELECT, "KEYCODE_BUTTON_SELECT"); - names.append(KEYCODE_BUTTON_MODE, "KEYCODE_BUTTON_MODE"); - names.append(KEYCODE_ESCAPE, "KEYCODE_ESCAPE"); - names.append(KEYCODE_FORWARD_DEL, "KEYCODE_FORWARD_DEL"); - names.append(KEYCODE_CTRL_LEFT, "KEYCODE_CTRL_LEFT"); - names.append(KEYCODE_CTRL_RIGHT, "KEYCODE_CTRL_RIGHT"); - names.append(KEYCODE_CAPS_LOCK, "KEYCODE_CAPS_LOCK"); - names.append(KEYCODE_SCROLL_LOCK, "KEYCODE_SCROLL_LOCK"); - names.append(KEYCODE_META_LEFT, "KEYCODE_META_LEFT"); - names.append(KEYCODE_META_RIGHT, "KEYCODE_META_RIGHT"); - names.append(KEYCODE_FUNCTION, "KEYCODE_FUNCTION"); - names.append(KEYCODE_SYSRQ, "KEYCODE_SYSRQ"); - names.append(KEYCODE_BREAK, "KEYCODE_BREAK"); - names.append(KEYCODE_MOVE_HOME, "KEYCODE_MOVE_HOME"); - names.append(KEYCODE_MOVE_END, "KEYCODE_MOVE_END"); - names.append(KEYCODE_INSERT, "KEYCODE_INSERT"); - names.append(KEYCODE_FORWARD, "KEYCODE_FORWARD"); - names.append(KEYCODE_MEDIA_PLAY, "KEYCODE_MEDIA_PLAY"); - names.append(KEYCODE_MEDIA_PAUSE, "KEYCODE_MEDIA_PAUSE"); - names.append(KEYCODE_MEDIA_CLOSE, "KEYCODE_MEDIA_CLOSE"); - names.append(KEYCODE_MEDIA_EJECT, "KEYCODE_MEDIA_EJECT"); - names.append(KEYCODE_MEDIA_RECORD, "KEYCODE_MEDIA_RECORD"); - names.append(KEYCODE_F1, "KEYCODE_F1"); - names.append(KEYCODE_F2, "KEYCODE_F2"); - names.append(KEYCODE_F3, "KEYCODE_F3"); - names.append(KEYCODE_F4, "KEYCODE_F4"); - names.append(KEYCODE_F5, "KEYCODE_F5"); - names.append(KEYCODE_F6, "KEYCODE_F6"); - names.append(KEYCODE_F7, "KEYCODE_F7"); - names.append(KEYCODE_F8, "KEYCODE_F8"); - names.append(KEYCODE_F9, "KEYCODE_F9"); - names.append(KEYCODE_F10, "KEYCODE_F10"); - names.append(KEYCODE_F11, "KEYCODE_F11"); - names.append(KEYCODE_F12, "KEYCODE_F12"); - names.append(KEYCODE_NUM_LOCK, "KEYCODE_NUM_LOCK"); - names.append(KEYCODE_NUMPAD_0, "KEYCODE_NUMPAD_0"); - names.append(KEYCODE_NUMPAD_1, "KEYCODE_NUMPAD_1"); - names.append(KEYCODE_NUMPAD_2, "KEYCODE_NUMPAD_2"); - names.append(KEYCODE_NUMPAD_3, "KEYCODE_NUMPAD_3"); - names.append(KEYCODE_NUMPAD_4, "KEYCODE_NUMPAD_4"); - names.append(KEYCODE_NUMPAD_5, "KEYCODE_NUMPAD_5"); - names.append(KEYCODE_NUMPAD_6, "KEYCODE_NUMPAD_6"); - names.append(KEYCODE_NUMPAD_7, "KEYCODE_NUMPAD_7"); - names.append(KEYCODE_NUMPAD_8, "KEYCODE_NUMPAD_8"); - names.append(KEYCODE_NUMPAD_9, "KEYCODE_NUMPAD_9"); - names.append(KEYCODE_NUMPAD_DIVIDE, "KEYCODE_NUMPAD_DIVIDE"); - names.append(KEYCODE_NUMPAD_MULTIPLY, "KEYCODE_NUMPAD_MULTIPLY"); - names.append(KEYCODE_NUMPAD_SUBTRACT, "KEYCODE_NUMPAD_SUBTRACT"); - names.append(KEYCODE_NUMPAD_ADD, "KEYCODE_NUMPAD_ADD"); - names.append(KEYCODE_NUMPAD_DOT, "KEYCODE_NUMPAD_DOT"); - names.append(KEYCODE_NUMPAD_COMMA, "KEYCODE_NUMPAD_COMMA"); - names.append(KEYCODE_NUMPAD_ENTER, "KEYCODE_NUMPAD_ENTER"); - names.append(KEYCODE_NUMPAD_EQUALS, "KEYCODE_NUMPAD_EQUALS"); - names.append(KEYCODE_NUMPAD_LEFT_PAREN, "KEYCODE_NUMPAD_LEFT_PAREN"); - names.append(KEYCODE_NUMPAD_RIGHT_PAREN, "KEYCODE_NUMPAD_RIGHT_PAREN"); - names.append(KEYCODE_VOLUME_MUTE, "KEYCODE_VOLUME_MUTE"); - names.append(KEYCODE_INFO, "KEYCODE_INFO"); - names.append(KEYCODE_CHANNEL_UP, "KEYCODE_CHANNEL_UP"); - names.append(KEYCODE_CHANNEL_DOWN, "KEYCODE_CHANNEL_DOWN"); - names.append(KEYCODE_ZOOM_IN, "KEYCODE_ZOOM_IN"); - names.append(KEYCODE_ZOOM_OUT, "KEYCODE_ZOOM_OUT"); - names.append(KEYCODE_TV, "KEYCODE_TV"); - names.append(KEYCODE_WINDOW, "KEYCODE_WINDOW"); - names.append(KEYCODE_GUIDE, "KEYCODE_GUIDE"); - names.append(KEYCODE_DVR, "KEYCODE_DVR"); - names.append(KEYCODE_BOOKMARK, "KEYCODE_BOOKMARK"); - names.append(KEYCODE_CAPTIONS, "KEYCODE_CAPTIONS"); - names.append(KEYCODE_SETTINGS, "KEYCODE_SETTINGS"); - names.append(KEYCODE_TV_POWER, "KEYCODE_TV_POWER"); - names.append(KEYCODE_TV_INPUT, "KEYCODE_TV_INPUT"); - names.append(KEYCODE_STB_INPUT, "KEYCODE_STB_INPUT"); - names.append(KEYCODE_STB_POWER, "KEYCODE_STB_POWER"); - names.append(KEYCODE_AVR_POWER, "KEYCODE_AVR_POWER"); - names.append(KEYCODE_AVR_INPUT, "KEYCODE_AVR_INPUT"); - names.append(KEYCODE_PROG_RED, "KEYCODE_PROG_RED"); - names.append(KEYCODE_PROG_GREEN, "KEYCODE_PROG_GREEN"); - names.append(KEYCODE_PROG_YELLOW, "KEYCODE_PROG_YELLOW"); - names.append(KEYCODE_PROG_BLUE, "KEYCODE_PROG_BLUE"); - names.append(KEYCODE_APP_SWITCH, "KEYCODE_APP_SWITCH"); - names.append(KEYCODE_BUTTON_1, "KEYCODE_BUTTON_1"); - names.append(KEYCODE_BUTTON_2, "KEYCODE_BUTTON_2"); - names.append(KEYCODE_BUTTON_3, "KEYCODE_BUTTON_3"); - names.append(KEYCODE_BUTTON_4, "KEYCODE_BUTTON_4"); - names.append(KEYCODE_BUTTON_5, "KEYCODE_BUTTON_5"); - names.append(KEYCODE_BUTTON_6, "KEYCODE_BUTTON_6"); - names.append(KEYCODE_BUTTON_7, "KEYCODE_BUTTON_7"); - names.append(KEYCODE_BUTTON_8, "KEYCODE_BUTTON_8"); - names.append(KEYCODE_BUTTON_9, "KEYCODE_BUTTON_9"); - names.append(KEYCODE_BUTTON_10, "KEYCODE_BUTTON_10"); - names.append(KEYCODE_BUTTON_11, "KEYCODE_BUTTON_11"); - names.append(KEYCODE_BUTTON_12, "KEYCODE_BUTTON_12"); - names.append(KEYCODE_BUTTON_13, "KEYCODE_BUTTON_13"); - names.append(KEYCODE_BUTTON_14, "KEYCODE_BUTTON_14"); - names.append(KEYCODE_BUTTON_15, "KEYCODE_BUTTON_15"); - names.append(KEYCODE_BUTTON_16, "KEYCODE_BUTTON_16"); - names.append(KEYCODE_LANGUAGE_SWITCH, "KEYCODE_LANGUAGE_SWITCH"); - names.append(KEYCODE_MANNER_MODE, "KEYCODE_MANNER_MODE"); - names.append(KEYCODE_3D_MODE, "KEYCODE_3D_MODE"); - names.append(KEYCODE_CONTACTS, "KEYCODE_CONTACTS"); - names.append(KEYCODE_CALENDAR, "KEYCODE_CALENDAR"); - names.append(KEYCODE_MUSIC, "KEYCODE_MUSIC"); - names.append(KEYCODE_CALCULATOR, "KEYCODE_CALCULATOR"); - names.append(KEYCODE_ZENKAKU_HANKAKU, "KEYCODE_ZENKAKU_HANKAKU"); - names.append(KEYCODE_EISU, "KEYCODE_EISU"); - names.append(KEYCODE_MUHENKAN, "KEYCODE_MUHENKAN"); - names.append(KEYCODE_HENKAN, "KEYCODE_HENKAN"); - names.append(KEYCODE_KATAKANA_HIRAGANA, "KEYCODE_KATAKANA_HIRAGANA"); - names.append(KEYCODE_YEN, "KEYCODE_YEN"); - names.append(KEYCODE_RO, "KEYCODE_RO"); - names.append(KEYCODE_KANA, "KEYCODE_KANA"); - names.append(KEYCODE_ASSIST, "KEYCODE_ASSIST"); - names.append(KEYCODE_BRIGHTNESS_DOWN, "KEYCODE_BRIGHTNESS_DOWN"); - names.append(KEYCODE_BRIGHTNESS_UP, "KEYCODE_BRIGHTNESS_UP"); - names.append(KEYCODE_MEDIA_AUDIO_TRACK, "KEYCODE_MEDIA_AUDIO_TRACK"); - names.append(KEYCODE_SLEEP, "KEYCODE_SLEEP"); - names.append(KEYCODE_WAKEUP, "KEYCODE_WAKEUP"); - }; - // Symbolic names of all metakeys in bit order from least significant to most significant. // Accordingly there are exactly 32 values in this table. private static final String[] META_SYMBOLIC_NAMES = new String[] { @@ -927,6 +694,8 @@ public class KeyEvent extends InputEvent implements Parcelable { "0x80000000", }; + private static final String LABEL_PREFIX = "KEYCODE_"; + /** * @deprecated There are now more than MAX_KEYCODE keycodes. * Use {@link #getMaxKeyCode()} instead. @@ -1178,20 +947,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 +969,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 +978,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 +1057,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 +1087,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 +1103,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 +1114,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,27 +1126,26 @@ 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. */ boolean onKeyMultiple(int keyCode, int count, KeyEvent event); } - static { - populateKeycodeSymbolicNames(); - } + private static native String nativeKeyCodeToString(int keyCode); + private static native int nativeKeyCodeFromString(String keyCode); private KeyEvent() { } /** * 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 +1159,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 +1182,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 +1207,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 +1236,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 +1267,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 +1303,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 +1341,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 +1460,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 +1470,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 +1490,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 +1515,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 +1551,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 @@ -1793,19 +1561,15 @@ public class KeyEvent extends InputEvent implements Parcelable { return mAction == ACTION_DOWN; } - /** - * 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 + /** Is this a system key? System keys can not be used for menu shortcuts. */ public final boolean isSystem() { - return native_isSystemKey(mKeyCode); + return isSystemKey(mKeyCode); } /** @hide */ - public final boolean hasDefaultAction() { - return native_hasDefaultAction(mKeyCode); + public final boolean isWakeKey() { + return isWakeKey(mKeyCode); } /** @@ -1851,16 +1615,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 +1629,83 @@ 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: + 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; + } + return false; + } + + + /** Is this a system key? System keys can not be used for menu shortcuts. + * @hide + */ + public static final boolean isSystemKey(int keyCode) { + switch (keyCode) { + case KeyEvent.KEYCODE_MENU: + case KeyEvent.KEYCODE_SOFT_RIGHT: + case KeyEvent.KEYCODE_HOME: case KeyEvent.KEYCODE_BACK: + case KeyEvent.KEYCODE_CALL: + case KeyEvent.KEYCODE_ENDCALL: + case KeyEvent.KEYCODE_VOLUME_UP: + case KeyEvent.KEYCODE_VOLUME_DOWN: + case KeyEvent.KEYCODE_VOLUME_MUTE: + case KeyEvent.KEYCODE_MUTE: + case KeyEvent.KEYCODE_POWER: + case KeyEvent.KEYCODE_HEADSETHOOK: + case KeyEvent.KEYCODE_MEDIA_PLAY: + case KeyEvent.KEYCODE_MEDIA_PAUSE: + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + 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: + case KeyEvent.KEYCODE_CAMERA: + case KeyEvent.KEYCODE_FOCUS: + case KeyEvent.KEYCODE_SEARCH: + case KeyEvent.KEYCODE_BRIGHTNESS_DOWN: + case KeyEvent.KEYCODE_BRIGHTNESS_UP: + case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK: + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_RIGHT: + case KeyEvent.KEYCODE_DPAD_DOWN: + case KeyEvent.KEYCODE_DPAD_LEFT: + return true; + } + + return false; + } + + /** @hide */ + public static final boolean isWakeKey(int keyCode) { + switch (keyCode) { + case KeyEvent.KEYCODE_BACK: + case KeyEvent.KEYCODE_POWER: + case KeyEvent.KEYCODE_MENU: + case KeyEvent.KEYCODE_SLEEP: + case KeyEvent.KEYCODE_WAKEUP: return true; - default: - return false; } + return false; } /** {@inheritDoc} */ @@ -2352,7 +2177,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 +2191,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 +2202,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 +2211,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 +2219,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 +2234,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 +2258,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 +2272,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 +2284,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 +2313,7 @@ public class KeyEvent extends InputEvent implements Parcelable { /** * Renamed to {@link #getDeviceId}. - * + * * @hide * @deprecated use {@link #getDeviceId()} instead. */ @@ -2520,7 +2345,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 +2368,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 +2392,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 +2407,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 +2422,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 +2435,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 +2459,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 +2468,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 +2476,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 +2551,7 @@ public class KeyEvent extends InputEvent implements Parcelable { int mDownKeyCode; Object mDownTarget; SparseIntArray mActiveLongPresses = new SparseIntArray(); - + /** * Reset back to initial state. */ @@ -2736,7 +2561,7 @@ public class KeyEvent extends InputEvent implements Parcelable { mDownTarget = null; mActiveLongPresses.clear(); } - + /** * Stop any tracking associated with this target. */ @@ -2747,14 +2572,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 +2592,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 +2600,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 +2610,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. @@ -2862,8 +2687,8 @@ public class KeyEvent extends InputEvent implements Parcelable { * @see KeyCharacterMap#getDisplayLabel */ public static String keyCodeToString(int keyCode) { - String symbolicName = KEYCODE_SYMBOLIC_NAMES.get(keyCode); - return symbolicName != null ? symbolicName : Integer.toString(keyCode); + String symbolicName = nativeKeyCodeToString(keyCode); + return symbolicName != null ? LABEL_PREFIX + symbolicName : Integer.toString(keyCode); } /** @@ -2875,17 +2700,13 @@ public class KeyEvent extends InputEvent implements Parcelable { * @see #keycodeToString(int) */ public static int keyCodeFromString(String symbolicName) { - if (symbolicName == null) { - throw new IllegalArgumentException("symbolicName must not be null"); + if (symbolicName.startsWith(LABEL_PREFIX)) { + symbolicName = symbolicName.substring(LABEL_PREFIX.length()); } - - final int count = KEYCODE_SYMBOLIC_NAMES.size(); - for (int i = 0; i < count; i++) { - if (symbolicName.equals(KEYCODE_SYMBOLIC_NAMES.valueAt(i))) { - return i; - } + int keyCode = nativeKeyCodeFromString(symbolicName); + if (keyCode > 0) { + return keyCode; } - try { return Integer.parseInt(symbolicName, 10); } catch (NumberFormatException ex) { @@ -2940,12 +2761,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(); @@ -2973,7 +2794,4 @@ public class KeyEvent extends InputEvent implements Parcelable { out.writeLong(mDownTime); out.writeLong(mEventTime); } - - private native boolean native_isSystemKey(int keyCode); - private native boolean native_hasDefaultAction(int keyCode); } diff --git a/core/java/android/view/LayoutInflater.java b/core/java/android/view/LayoutInflater.java index cd905fa..b9ed801 100644 --- a/core/java/android/view/LayoutInflater.java +++ b/core/java/android/view/LayoutInflater.java @@ -21,13 +21,16 @@ import android.os.Handler; import android.os.Message; import android.os.Trace; import android.widget.FrameLayout; + import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import android.content.Context; +import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.util.AttributeSet; +import android.util.Log; import android.util.Xml; import java.io.IOException; @@ -61,7 +64,8 @@ import java.util.HashMap; * @see Context#getSystemService */ public abstract class LayoutInflater { - private final boolean DEBUG = false; + private static final String TAG = LayoutInflater.class.getSimpleName(); + private static final boolean DEBUG = false; /** * This field should be made private, so it is hidden from the SDK. @@ -90,6 +94,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 @@ -391,8 +399,13 @@ public abstract class LayoutInflater { * the inflated XML file. */ public View inflate(int resource, ViewGroup root, boolean attachToRoot) { - if (DEBUG) System.out.println("INFLATING from resource: " + resource); - XmlResourceParser parser = getContext().getResources().getLayout(resource); + final Resources res = getContext().getResources(); + if (DEBUG) { + Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" (" + + Integer.toHexString(resource) + ")"); + } + + final XmlResourceParser parser = res.getLayout(resource); try { return inflate(parser, root, attachToRoot); } finally { @@ -459,15 +472,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 +497,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 +678,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 +766,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 +789,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 +810,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 +825,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 +883,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 +909,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/MotionEvent.java b/core/java/android/view/MotionEvent.java index 6378ffd..0626ab9 100644 --- a/core/java/android/view/MotionEvent.java +++ b/core/java/android/view/MotionEvent.java @@ -167,6 +167,7 @@ import android.util.SparseArray; */ public final class MotionEvent extends InputEvent implements Parcelable { private static final long NS_PER_MS = 1000000; + private static final String LABEL_PREFIX = "AXIS_"; /** * An invalid pointer id. @@ -1369,6 +1370,9 @@ public final class MotionEvent extends InputEvent implements Parcelable { private static native long nativeReadFromParcel(long nativePtr, Parcel parcel); private static native void nativeWriteToParcel(long nativePtr, Parcel parcel); + private static native String nativeAxisToString(int axis); + private static native int nativeAxisFromString(String label); + private MotionEvent() { } @@ -3051,8 +3055,8 @@ public final class MotionEvent extends InputEvent implements Parcelable { * @return The symbolic name of the specified axis. */ public static String axisToString(int axis) { - String symbolicName = AXIS_SYMBOLIC_NAMES.get(axis); - return symbolicName != null ? symbolicName : Integer.toString(axis); + String symbolicName = nativeAxisToString(axis); + return symbolicName != null ? LABEL_PREFIX + symbolicName : Integer.toString(axis); } /** @@ -3064,17 +3068,13 @@ public final class MotionEvent extends InputEvent implements Parcelable { * @see KeyEvent#keyCodeToString(int) */ public static int axisFromString(String symbolicName) { - if (symbolicName == null) { - throw new IllegalArgumentException("symbolicName must not be null"); + if (symbolicName.startsWith(LABEL_PREFIX)) { + symbolicName = symbolicName.substring(LABEL_PREFIX.length()); } - - final int count = AXIS_SYMBOLIC_NAMES.size(); - for (int i = 0; i < count; i++) { - if (symbolicName.equals(AXIS_SYMBOLIC_NAMES.valueAt(i))) { - return i; - } + int axis = nativeAxisFromString(symbolicName); + if (axis >= 0) { + return axis; } - try { return Integer.parseInt(symbolicName, 10); } catch (NumberFormatException ex) { 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..8b80c3e0 --- /dev/null +++ b/core/java/android/view/RenderNode.java @@ -0,0 +1,961 @@ +/* + * 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; + +import java.util.ArrayList; +import java.util.List; + +/** + * <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; + + // We need to keep a strong reference to all running animators to ensure that + // they can call removeAnimator when they have finished, as the native-side + // object can only hold a WeakReference<> to avoid leaking memory due to + // cyclic references. + private List<RenderNodeAnimator> mActiveAnimators; + + private RenderNode(String name) { + mNativeRenderNode = nCreate(name); + } + + /** + * @see RenderNode#adopt(long) + */ + private RenderNode(long nativePtr) { + mNativeRenderNode = nativePtr; + } + + /** + * 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); + } + + /** + * Adopts an existing native render node. + * + * Note: This will *NOT* incRef() on the native object, however it will + * decRef() when it is destroyed. The caller should have already incRef'd it + */ + public static RenderNode adopt(long nativePtr) { + return new RenderNode(nativePtr); + } + + + /** + * 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); + } + + public void setElevation(float lift) { + nSetElevation(mNativeRenderNode, lift); + } + + public float getElevation() { + return nGetElevation(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); + } + + /////////////////////////////////////////////////////////////////////////// + // Animations + /////////////////////////////////////////////////////////////////////////// + + public void addAnimator(RenderNodeAnimator animator) { + if (mActiveAnimators == null) { + mActiveAnimators = new ArrayList<RenderNodeAnimator>(); + } + mActiveAnimators.add(animator); + nAddAnimator(mNativeRenderNode, animator.getNativeAnimator()); + } + + public void removeAnimator(RenderNodeAnimator animator) { + nRemoveAnimator(mNativeRenderNode, animator.getNativeAnimator()); + mActiveAnimators.remove(animator); + } + + /////////////////////////////////////////////////////////////////////////// + // 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 nSetElevation(long renderNode, float lift); + 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 nGetElevation(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); + + /////////////////////////////////////////////////////////////////////////// + // Animations + /////////////////////////////////////////////////////////////////////////// + + private static native void nAddAnimator(long renderNode, long animatorPtr); + private static native void nRemoveAnimator(long renderNode, long animatorPtr); + + /////////////////////////////////////////////////////////////////////////// + // Finalization + /////////////////////////////////////////////////////////////////////////// + + @Override + protected void finalize() throws Throwable { + try { + nDestroyRenderNode(mNativeRenderNode); + } finally { + super.finalize(); + } + } +} diff --git a/core/java/android/view/RenderNodeAnimator.java b/core/java/android/view/RenderNodeAnimator.java new file mode 100644 index 0000000..b70ae3d --- /dev/null +++ b/core/java/android/view/RenderNodeAnimator.java @@ -0,0 +1,122 @@ +/* + * 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.util.SparseIntArray; + +import java.lang.ref.WeakReference; + +/** + * @hide + */ +public final class RenderNodeAnimator { + + // Keep in sync with enum RenderProperty in Animator.h + private static final int TRANSLATION_X = 0; + private static final int TRANSLATION_Y = 1; + private static final int TRANSLATION_Z = 2; + private static final int SCALE_X = 3; + private static final int SCALE_Y = 4; + private static final int ROTATION = 5; + private static final int ROTATION_X = 6; + private static final int ROTATION_Y = 7; + private static final int X = 8; + private static final int Y = 9; + private static final int Z = 10; + private static final int ALPHA = 11; + + // ViewPropertyAnimator uses a mask for its values, we need to remap them + // to the enum values here. RenderPropertyAnimator can't use the mask values + // directly as internally it uses a lookup table so it needs the values to + // be sequential starting from 0 + private static final SparseIntArray sViewPropertyAnimatorMap = new SparseIntArray(15) {{ + put(ViewPropertyAnimator.TRANSLATION_X, TRANSLATION_X); + put(ViewPropertyAnimator.TRANSLATION_Y, TRANSLATION_Y); + put(ViewPropertyAnimator.TRANSLATION_Z, TRANSLATION_Z); + put(ViewPropertyAnimator.SCALE_X, SCALE_X); + put(ViewPropertyAnimator.SCALE_Y, SCALE_Y); + put(ViewPropertyAnimator.ROTATION, ROTATION); + put(ViewPropertyAnimator.ROTATION_X, ROTATION_X); + put(ViewPropertyAnimator.ROTATION_Y, ROTATION_Y); + put(ViewPropertyAnimator.X, X); + put(ViewPropertyAnimator.Y, Y); + put(ViewPropertyAnimator.Z, Z); + put(ViewPropertyAnimator.ALPHA, ALPHA); + }}; + + // Keep in sync DeltaValueType in Animator.h + private static final int DELTA_TYPE_ABSOLUTE = 0; + private static final int DELTA_TYPE_DELTA = 1; + + private RenderNode mTarget; + private long mNativePtr; + + public int mapViewPropertyToRenderProperty(int viewProperty) { + return sViewPropertyAnimatorMap.get(viewProperty); + } + + public RenderNodeAnimator(int property, int deltaType, float deltaValue) { + mNativePtr = nCreateAnimator(new WeakReference<RenderNodeAnimator>(this), + property, deltaType, deltaValue); + } + + public void start(View target) { + mTarget = target.mRenderNode; + mTarget.addAnimator(this); + // Kick off a frame to start the process + target.invalidateViewProperty(true, false); + } + + public void cancel() { + mTarget.removeAnimator(this); + } + + public void setDuration(int duration) { + nSetDuration(mNativePtr, duration); + } + + long getNativeAnimator() { + return mNativePtr; + } + + private void onFinished() { + mTarget.removeAnimator(this); + } + + // Called by native + private static void callOnFinished(WeakReference<RenderNodeAnimator> weakThis) { + RenderNodeAnimator animator = weakThis.get(); + if (animator != null) { + animator.onFinished(); + } + } + + @Override + protected void finalize() throws Throwable { + try { + nUnref(mNativePtr); + mNativePtr = 0; + } finally { + super.finalize(); + } + } + + private static native long nCreateAnimator(WeakReference<RenderNodeAnimator> weakThis, + int property, int deltaValueType, float deltaValue); + private static native void nSetDuration(long nativePtr, int duration); + private static native void nUnref(long nativePtr); +} 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..4a2cc1a 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 @@ -416,7 +422,8 @@ public class SurfaceView extends View { mWindowType = type; } - private void updateWindow(boolean force, boolean redrawNeeded) { + /** @hide */ + protected void updateWindow(boolean force, boolean redrawNeeded) { if (!mHaveFrame) { return; } 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..eaec8ab --- /dev/null +++ b/core/java/android/view/ThreadedRenderer.java @@ -0,0 +1,319 @@ +/* + * 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(); + + private int mWidth, mHeight; + private long mNativeProxy; + private boolean mInitialized = false; + private RenderNode mRootNode; + + ThreadedRenderer(boolean translucent) { + long rootNodePtr = nCreateRootRenderNode(); + mRootNode = RenderNode.adopt(rootNodePtr); + mRootNode.setClipToBounds(false); + mNativeProxy = nCreateProxy(translucent, rootNodePtr); + } + + @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); + try { + callbacks.onHardwarePostDraw(canvas); + canvas.drawDisplayList(view.getDisplayList()); + callbacks.onHardwarePostDraw(canvas); + } finally { + 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); + + attachInfo.mIgnoreDirtyState = false; + + if (dirty == null) { + dirty = NULL_RECT; + } + nSyncAndDrawFrame(mNativeProxy, 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 + 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 nCreateRootRenderNode(); + private static native long nCreateProxy(boolean translucent, long rootRenderNode); + 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 nSyncAndDrawFrame(long nativeProxy, + 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..6afff4d 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -16,18 +16,23 @@ 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; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Bitmap; -import android.graphics.Camera; import android.graphics.Canvas; 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 +91,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 +102,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 +667,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 +676,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 @@ -708,6 +719,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private static boolean sIgnoreMeasureCache = false; /** + * Ignore the clipBounds of this view for the children. + */ + static boolean sIgnoreClipBoundsForChildren = false; + + /** * This view does not want keystrokes. Use with TAKES_FOCUS_MASK when * calling setFlags. */ @@ -729,6 +745,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 +917,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 +966,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 +1056,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 +1077,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 +1654,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 +1796,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 +1868,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 +2065,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 +2330,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,6 +2374,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ static final int PFLAG3_CALLED_SUPER = 0x10; + /** + * Flag indicating that a view's outline has been specifically defined. + */ + static final int PFLAG3_OUTLINE_DEFINED = 0x20; /** * Flag indicating that we're in the process of applying window insets. @@ -2279,6 +2389,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ static final int PFLAG3_FITTING_SYSTEM_WINDOWS = 0x80; + /** + * Flag indicating that nested scrolling is enabled for this view. + * The view will optionally cooperate with views up its parent chain to allow for + * integrated nested scrolling along the same axis. + */ + static final int PFLAG3_NESTED_SCROLLING_ENABLED = 0x200; + /* End of masks for mPrivateFlags3 */ static final int DRAG_MASK = PFLAG2_DRAG_CAN_ACCEPT | PFLAG2_DRAG_HOVERED; @@ -2674,6 +2791,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 +2818,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) * @@ -2725,6 +2848,21 @@ public class View implements Drawable.Callback, KeyEvent.Callback, public static final int SCREEN_STATE_ON = 0x1; /** + * Indicates no axis of view scrolling. + */ + public static final int SCROLL_AXIS_NONE = 0; + + /** + * Indicates scrolling along the horizontal axis. + */ + public static final int SCROLL_AXIS_HORIZONTAL = 1 << 0; + + /** + * Indicates scrolling along the vertical axis. + */ + public static final int SCROLL_AXIS_VERTICAL = 1 << 1; + + /** * Controls the over-scroll mode for this view. * See {@link #overScrollBy(int, int, int, int, int, int, int, int, boolean)}, * {@link #OVER_SCROLL_ALWAYS}, {@link #OVER_SCROLL_IF_CONTENT_SCROLLS}, @@ -2810,120 +2948,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. */ @@ -2943,17 +2984,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, /** * Current clip bounds. to which all drawing of this view are constrained. */ - private Rect mClipBounds = null; + Rect mClipBounds = null; 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 +3170,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 +3252,12 @@ 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. + */ + 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 +3460,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 @@ -3430,6 +3487,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback, ViewOverlay mOverlay; /** + * The currently active parent view for receiving delegated nested scrolling events. + * This is set by {@link #startNestedScroll(int)} during a touch interaction and cleared + * by {@link #stopNestedScroll()} at the same point where we clear + * requestDisallowInterceptTouchEvent. + */ + private ViewParent mNestedScrollingParent; + + /** * Consistency verifier for debugging purposes. * @hide */ @@ -3439,6 +3504,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1); + private int[] mTempNestedScrollConsumed; + /** * Simple constructor to use when creating a view from code. * @@ -3461,6 +3528,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; @@ -3472,6 +3540,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, // of whether a layout was requested on that View. sIgnoreMeasureCache = targetSdkVersion < KITKAT; + // Older apps may need this to ignore the clip bounds + sIgnoreClipBoundsForChildren = targetSdkVersion < L; + sCompatibilityDone = true; } } @@ -3497,27 +3568,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 +3648,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, float tx = 0; float ty = 0; + float tz = 0; + float elevation = 0; float rotation = 0; float rotationX = 0; float rotationY = 0; @@ -3619,6 +3729,14 @@ 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_elevation: + elevation = a.getDimensionPixelOffset(attr, 0); + transformSet = true; + break; case com.android.internal.R.styleable.View_rotation: rotation = a.getFloat(attr, 0); transformSet = true; @@ -3871,6 +3989,12 @@ 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; + case R.styleable.View_nestedScrollingEnabled: + setNestedScrollingEnabled(a.getBoolean(attr, false)); + break; } } @@ -3960,6 +4084,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (transformSet) { setTranslationX(tx); setTranslationY(ty); + setTranslationZ(tz); + setElevation(elevation); setRotation(rotation); setRotationX(rotationX); setRotationY(rotationY); @@ -3979,6 +4105,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ View() { mResources = null; + mRenderNode = RenderNode.create(getClass().getName()); } public String toString() { @@ -4026,7 +4153,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 +4735,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 +4753,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 (!focused && v != null) { + v.getBoundsOnScreen(r); + final int[] location = new int[2]; + getLocationOnScreen(location); + r.offset(-location[0], -location[1]); + } else { + r.set(0, 0, mRight - mLeft, mBottom - mTop); + } + + 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 +4875,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, System.out.println(this + " clearFocus()"); } - clearFocusInternal(true, true); + clearFocusInternal(null, true, true); } /** @@ -4729,7 +4887,7 @@ 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; @@ -4739,6 +4897,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, onFocusChanged(false, 0, null); + manageFocusHotspot(false, focused); refreshDrawableState(); if (propagate && (!refocus || !rootViewRequestFocus())) { @@ -4766,12 +4925,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 +4978,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 +5041,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 +5658,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 +5836,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 +5854,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 +6071,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 +6088,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 +6313,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 +6325,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 +6484,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 +6507,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 +6537,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 +6907,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 +6926,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 +6938,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 +6988,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 +7002,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 +7022,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 +7052,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 +7100,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 +7447,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 +7531,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 +7545,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 +7567,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 +7586,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 +7904,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @hide */ public void dispatchStartTemporaryDetach() { - clearDisplayList(); - onStartTemporaryDetach(); } @@ -7825,27 +8009,46 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @return True if the event was handled by the view, false otherwise. */ public boolean dispatchTouchEvent(MotionEvent event) { + boolean result = false; + if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(event, 0); } + final int actionMasked = event.getActionMasked(); + if (actionMasked == MotionEvent.ACTION_DOWN) { + // Defensive cleanup for new gesture + stopNestedScroll(); + } + if (onFilterTouchEventForSecurity(event)) { //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo; - if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED + if (li != null && li.mOnTouchListener != null + && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { - return true; + result = true; } - if (onTouchEvent(event)) { - return true; + if (!result && onTouchEvent(event)) { + result = true; } } - if (mInputEventConsistencyVerifier != null) { + if (!result && mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); } - return false; + + // Clean up after nested scrolls if this is the end of a gesture; + // also cancel it if we tried an ACTION_DOWN but we didn't want the rest + // of the gesture. + if (actionMasked == MotionEvent.ACTION_UP || + actionMasked == MotionEvent.ACTION_CANCEL || + (actionMasked == MotionEvent.ACTION_DOWN && !result)) { + stopNestedScroll(); + } + + return result; } /** @@ -8080,7 +8283,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 +8295,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 +8314,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 +8327,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 +8338,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 +8352,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 +8364,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 +8525,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 +8567,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 +8645,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 +8841,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 +8963,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 +9002,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 +9035,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 +9059,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 +9093,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 +9151,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 +9314,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 +9326,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 +9630,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 +9643,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 +9652,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 +9660,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 +9677,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 +9721,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 +9741,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 +9759,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 +9780,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 +9803,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 +9823,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 +9846,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 +9867,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 +9881,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 +9902,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 +9916,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 +9939,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 +9958,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 +9981,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 +9999,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 +10082,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 +10107,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 +10128,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 +10175,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 +10198,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 +10243,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 +10263,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 +10299,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 +10322,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 +10358,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 +10378,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 +10404,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 +10427,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(); } /** @@ -10394,6 +10441,48 @@ public class View implements Drawable.Callback, KeyEvent.Callback, setTranslationY(y - mTop); } + /** + * The visual z position of this view, in pixels. This is equivalent to the + * {@link #setTranslationZ(float) translationZ} property plus the current + * {@link #getElevation() elevation} property. + * + * @return The visual z position of this view, in pixels. + */ + @ViewDebug.ExportedProperty(category = "drawing") + public float getZ() { + return getElevation() + getTranslationZ(); + } + + /** + * Sets the visual z position of this view, in pixels. This is equivalent to setting the + * {@link #setTranslationZ(float) translationZ} property to be the difference between + * the x value passed in and the current {@link #getElevation() elevation} property. + * + * @param z The visual z position of this view, in pixels. + */ + public void setZ(float z) { + setTranslationZ(z - getElevation()); + } + + @ViewDebug.ExportedProperty(category = "drawing") + public float getElevation() { + return mRenderNode.getElevation(); + } + + /** + * Sets the base depth location of this view. + * + * @attr ref android.R.styleable#View_elevation + */ + public void setElevation(float elevation) { + if (elevation != getElevation()) { + invalidateViewProperty(true, false); + mRenderNode.setElevation(elevation); + invalidateViewProperty(false, true); + + invalidateParentIfNeededAndWasQuickRejected(); + } + } /** * The horizontal location of this view relative to its {@link #getLeft() left} position. @@ -10404,7 +10493,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 +10507,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 +10526,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 +10540,154 @@ 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); + mRenderNode.setTranslationY(translationY); + invalidateViewProperty(false, true); + + invalidateParentIfNeededAndWasQuickRejected(); + } + } + + /** + * The depth location of this view relative to its {@link #getElevation() elevation}. + * + * @return The depth of this view relative to its elevation. + */ + @ViewDebug.ExportedProperty(category = "drawing") + public float getTranslationZ() { + return mRenderNode.getTranslationZ(); + } + + /** + * Sets the depth location of this view relative to its {@link #getElevation() elevation}. + * + * @attr ref android.R.styleable#View_translationZ + */ + public void setTranslationZ(float translationZ) { + if (translationZ != getTranslationZ()) { invalidateViewProperty(true, false); - info.mTranslationY = translationY; - info.mMatrixDirty = true; + mRenderNode.setTranslationZ(translationZ); invalidateViewProperty(false, true); - if (mDisplayList != null) { - mDisplayList.setTranslationY(translationY); + + 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. + * <p> + * If the outline is not set or is null, shadows will be cast from the + * bounds of the View. + * + * @param outline The new outline of the view. + * Must be {@link android.graphics.Outline#isValid() valid.} + */ + 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); + } + + // TODO: remove + public final boolean getClipToOutline() { return false; } + public void setClipToOutline(boolean clipToOutline) {} + + private void queryOutlineFromBackgroundIfUndefined() { + if ((mPrivateFlags3 & PFLAG3_OUTLINE_DEFINED) == 0) { + // Outline not currently defined, query from background + if (mOutline == null) { + mOutline = new Outline(); + } else { + //invalidate outline, to ensure background calculates it + mOutline.set(null); + } + if (mBackground.getOutline(mOutline)) { + if (!mOutline.isValid()) { + throw new IllegalStateException("Background drawable failed to build outline"); + } + mRenderNode.setOutline(mOutline); + } else { + mRenderNode.setOutline(null); } } } /** + * 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 +10776,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 +10806,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 +10825,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 +10852,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 +11124,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 +11179,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() && getZ() != 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 +11296,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 +11307,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 && getZ() != 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 +11379,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 +11462,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 +11585,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 +12078,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 +12102,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 +12494,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, InputMethodManager imm = InputMethodManager.peekInstance(); imm.focusIn(this); } - - if (mDisplayList != null) { - mDisplayList.clearDirty(); - } } /** @@ -12365,7 +12600,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,13 +12794,31 @@ 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; + if (mBackground != null) { + mBackground.clearHotspots(); + } + removeUnsetPressCallback(); removeLongPressCallback(); removePerformClickCallback(); removeSendViewScrolledAccessibilityEventCallback(); + stopNestedScroll(); destroyDrawingCache(); destroyLayer(false); @@ -12576,15 +12829,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 +12999,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } onDetachedFromWindow(); + onDetachedFromWindowInternal(); ListenerInfo li = mListenerInfo; final CopyOnWriteArrayList<OnAttachStateChangeListener> listeners = @@ -13053,11 +13301,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 +13358,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 +13418,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 +13447,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 +13456,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 +13467,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 +13498,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 +13617,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 +13655,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 +13704,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 +14327,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 +14349,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 +14362,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 +14411,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 +14451,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 +14743,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 +14910,88 @@ 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; + queryOutlineFromBackgroundIfUndefined(); + } + + // 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); + try { + drawable.draw(canvas); + } finally { + 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 +15260,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,14 +15324,19 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @param drawable the drawable to invalidate */ - public void invalidateDrawable(Drawable drawable) { + @Override + public void invalidateDrawable(@NonNull Drawable drawable) { if (verifyDrawable(drawable)) { - final Rect dirty = drawable.getBounds(); + final Rect dirty = drawable.getDirtyBounds(); final int scrollX = mScrollX; final int scrollY = mScrollY; invalidate(dirty.left + scrollX, dirty.top + scrollY, dirty.right + scrollX, dirty.bottom + scrollY); + + if (drawable == mBackground) { + queryOutlineFromBackgroundIfUndefined(); + } } } @@ -15113,6 +15348,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 +15368,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 +15438,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @hide */ - public void onResolveDrawables(int layoutDirection) { + public void onResolveDrawables(@ResolvedLayoutDir int layoutDirection) { } /** @@ -15249,7 +15485,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 +15670,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 +16205,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 +16226,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 +16562,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 +16593,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() @@ -17739,6 +17985,269 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * Enable or disable nested scrolling for this view. + * + * <p>If this property is set to true the view will be permitted to initiate nested + * scrolling operations with a compatible parent view in the current hierarchy. If this + * view does not implement nested scrolling this will have no effect. Disabling nested scrolling + * while a nested scroll is in progress has the effect of {@link #stopNestedScroll() stopping} + * the nested scroll.</p> + * + * @param enabled true to enable nested scrolling, false to disable + * + * @see #isNestedScrollingEnabled() + */ + public void setNestedScrollingEnabled(boolean enabled) { + if (enabled) { + mPrivateFlags3 |= PFLAG3_NESTED_SCROLLING_ENABLED; + } else { + stopNestedScroll(); + mPrivateFlags3 &= ~PFLAG3_NESTED_SCROLLING_ENABLED; + } + } + + /** + * Returns true if nested scrolling is enabled for this view. + * + * <p>If nested scrolling is enabled and this View class implementation supports it, + * this view will act as a nested scrolling child view when applicable, forwarding data + * about the scroll operation in progress to a compatible and cooperating nested scrolling + * parent.</p> + * + * @return true if nested scrolling is enabled + * + * @see #setNestedScrollingEnabled(boolean) + */ + public boolean isNestedScrollingEnabled() { + return (mPrivateFlags3 & PFLAG3_NESTED_SCROLLING_ENABLED) == + PFLAG3_NESTED_SCROLLING_ENABLED; + } + + /** + * Begin a nestable scroll operation along the given axes. + * + * <p>A view starting a nested scroll promises to abide by the following contract:</p> + * + * <p>The view will call startNestedScroll upon initiating a scroll operation. In the case + * of a touch scroll this corresponds to the initial {@link MotionEvent#ACTION_DOWN}. + * In the case of touch scrolling the nested scroll will be terminated automatically in + * the same manner as {@link ViewParent#requestDisallowInterceptTouchEvent(boolean)}. + * In the event of programmatic scrolling the caller must explicitly call + * {@link #stopNestedScroll()} to indicate the end of the nested scroll.</p> + * + * <p>If <code>startNestedScroll</code> returns true, a cooperative parent was found. + * If it returns false the caller may ignore the rest of this contract until the next scroll. + * Calling startNestedScroll while a nested scroll is already in progress will return true.</p> + * + * <p>At each incremental step of the scroll the caller should invoke + * {@link #dispatchNestedPreScroll(int, int, int[], int[]) dispatchNestedPreScroll} + * once it has calculated the requested scrolling delta. If it returns true the nested scrolling + * parent at least partially consumed the scroll and the caller should adjust the amount it + * scrolls by.</p> + * + * <p>After applying the remainder of the scroll delta the caller should invoke + * {@link #dispatchNestedScroll(int, int, int, int, int[]) dispatchNestedScroll}, passing + * both the delta consumed and the delta unconsumed. A nested scrolling parent may treat + * these values differently. See {@link ViewParent#onNestedScroll(View, int, int, int, int)}. + * </p> + * + * @param axes Flags consisting of a combination of {@link #SCROLL_AXIS_HORIZONTAL} and/or + * {@link #SCROLL_AXIS_VERTICAL}. + * @return true if a cooperative parent was found and nested scrolling has been enabled for + * the current gesture. + * + * @see #stopNestedScroll() + * @see #dispatchNestedPreScroll(int, int, int[], int[]) + * @see #dispatchNestedScroll(int, int, int, int, int[]) + */ + public boolean startNestedScroll(int axes) { + if (hasNestedScrollingParent()) { + // Already in progress + return true; + } + if (isNestedScrollingEnabled()) { + ViewParent p = getParent(); + View child = this; + while (p != null) { + try { + if (p.onStartNestedScroll(child, this, axes)) { + mNestedScrollingParent = p; + p.onNestedScrollAccepted(child, this, axes); + return true; + } + } catch (AbstractMethodError e) { + Log.e(VIEW_LOG_TAG, "ViewParent " + p + " does not implement interface " + + "method onStartNestedScroll", e); + // Allow the search upward to continue + } + if (p instanceof View) { + child = (View) p; + } + p = p.getParent(); + } + } + return false; + } + + /** + * Stop a nested scroll in progress. + * + * <p>Calling this method when a nested scroll is not currently in progress is harmless.</p> + * + * @see #startNestedScroll(int) + */ + public void stopNestedScroll() { + if (mNestedScrollingParent != null) { + mNestedScrollingParent.onStopNestedScroll(this); + mNestedScrollingParent = null; + } + } + + /** + * Returns true if this view has a nested scrolling parent. + * + * <p>The presence of a nested scrolling parent indicates that this view has initiated + * a nested scroll and it was accepted by an ancestor view further up the view hierarchy.</p> + * + * @return whether this view has a nested scrolling parent + */ + public boolean hasNestedScrollingParent() { + return mNestedScrollingParent != null; + } + + /** + * Dispatch one step of a nested scroll in progress. + * + * <p>Implementations of views that support nested scrolling should call this to report + * info about a scroll in progress to the current nested scrolling parent. If a nested scroll + * is not currently in progress or nested scrolling is not + * {@link #isNestedScrollingEnabled() enabled} for this view this method does nothing.</p> + * + * <p>Compatible View implementations should also call + * {@link #dispatchNestedPreScroll(int, int, int[], int[]) dispatchNestedPreScroll} before + * consuming a component of the scroll event themselves.</p> + * + * @param dxConsumed Horizontal distance in pixels consumed by this view during this scroll step + * @param dyConsumed Vertical distance in pixels consumed by this view during this scroll step + * @param dxUnconsumed Horizontal scroll distance in pixels not consumed by this view + * @param dyUnconsumed Horizontal scroll distance in pixels not consumed by this view + * @param offsetInWindow Optional. If not null, on return this will contain the offset + * in local view coordinates of this view from before this operation + * to after it completes. View implementations may use this to adjust + * expected input coordinate tracking. + * @return true if the event was dispatched, false if it could not be dispatched. + * @see #dispatchNestedPreScroll(int, int, int[], int[]) + */ + public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, + int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { + if (isNestedScrollingEnabled() && mNestedScrollingParent != null) { + if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) { + int startX = 0; + int startY = 0; + if (offsetInWindow != null) { + getLocationInWindow(offsetInWindow); + startX = offsetInWindow[0]; + startY = offsetInWindow[1]; + } + + mNestedScrollingParent.onNestedScroll(this, dxConsumed, dyConsumed, + dxUnconsumed, dyUnconsumed); + + if (offsetInWindow != null) { + getLocationInWindow(offsetInWindow); + offsetInWindow[0] -= startX; + offsetInWindow[1] -= startY; + } + return true; + } else if (offsetInWindow != null) { + // No motion, no dispatch. Keep offsetInWindow up to date. + offsetInWindow[0] = 0; + offsetInWindow[1] = 0; + } + } + return false; + } + + /** + * Dispatch one step of a nested scroll in progress before this view consumes any portion of it. + * + * <p>Nested pre-scroll events are to nested scroll events what touch intercept is to touch. + * <code>dispatchNestedPreScroll</code> offers an opportunity for the parent view in a nested + * scrolling operation to consume some or all of the scroll operation before the child view + * consumes it.</p> + * + * @param dx Horizontal scroll distance in pixels + * @param dy Vertical scroll distance in pixels + * @param consumed Output. If not null, consumed[0] will contain the consumed component of dx + * and consumed[1] the consumed dy. + * @param offsetInWindow Optional. If not null, on return this will contain the offset + * in local view coordinates of this view from before this operation + * to after it completes. View implementations may use this to adjust + * expected input coordinate tracking. + * @return true if the parent consumed some or all of the scroll delta + * @see #dispatchNestedScroll(int, int, int, int, int[]) + */ + public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { + if (isNestedScrollingEnabled() && mNestedScrollingParent != null) { + if (dx != 0 || dy != 0) { + int startX = 0; + int startY = 0; + if (offsetInWindow != null) { + getLocationInWindow(offsetInWindow); + startX = offsetInWindow[0]; + startY = offsetInWindow[1]; + } + + if (consumed == null) { + if (mTempNestedScrollConsumed == null) { + mTempNestedScrollConsumed = new int[2]; + } + consumed = mTempNestedScrollConsumed; + } + consumed[0] = 0; + consumed[1] = 0; + mNestedScrollingParent.onNestedPreScroll(this, dx, dy, consumed); + + if (offsetInWindow != null) { + getLocationInWindow(offsetInWindow); + offsetInWindow[0] -= startX; + offsetInWindow[1] -= startY; + } + return consumed[0] != 0 || consumed[1] != 0; + } else if (offsetInWindow != null) { + offsetInWindow[0] = 0; + offsetInWindow[1] = 0; + } + } + return false; + } + + /** + * Dispatch a fling to a nested scrolling parent. + * + * <p>This method should be used to indicate that a nested scrolling child has detected + * suitable conditions for a fling. Generally this means that a touch scroll has ended with a + * {@link VelocityTracker velocity} in the direction of scrolling that meets or exceeds + * the {@link ViewConfiguration#getScaledMinimumFlingVelocity() minimum fling velocity} + * along a scrollable axis.</p> + * + * <p>If a nested scrolling child view would normally fling but it is at the edge of + * its own content, it can use this method to delegate the fling to its nested scrolling + * parent instead. The parent may optionally consume the fling or observe a child fling.</p> + * + * @param velocityX Horizontal fling velocity in pixels per second + * @param velocityY Vertical fling velocity in pixels per second + * @param consumed true if the child consumed the fling, false otherwise + * @return true if the nested scrolling parent consumed or otherwise reacted to the fling + */ + public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { + if (isNestedScrollingEnabled() && mNestedScrollingParent != null) { + return mNestedScrollingParent.onNestedFling(this, velocityX, velocityY, consumed); + } + return false; + } + + /** * Gets a scale factor that determines the distance the view should scroll * vertically in response to {@link MotionEvent#ACTION_SCROLL}. * @return The vertical scroll scale factor. @@ -18020,6 +18529,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 +18553,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 +18594,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 +18757,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 +18836,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. */ @@ -18330,6 +18884,22 @@ public class View implements Drawable.Callback, KeyEvent.Callback, }; /** + * A Property wrapper around the <code>z</code> functionality handled by the + * {@link View#setZ(float)} and {@link View#getZ()} methods. + */ + public static final Property<View, Float> Z = new FloatProperty<View>("z") { + @Override + public void setValue(View object, float value) { + object.setZ(value); + } + + @Override + public Float get(View object) { + return object.getZ(); + } + }; + + /** * A Property wrapper around the <code>rotation</code> functionality handled by the * {@link View#setRotation(float)} and {@link View#getRotation()} methods. */ @@ -18552,10 +19122,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 +19141,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 +19179,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 +19433,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 +19528,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, final Callbacks mRootCallbacks; - HardwareCanvas mHardwareCanvas; - IWindowId mIWindowId; WindowId mWindowId; @@ -18932,7 +19537,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, View mRootView; IBinder mPanelParentWindowToken; - Surface mSurface; boolean mHardwareAccelerated; boolean mHardwareAccelerationRequested; @@ -19177,7 +19781,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 4b8541e..20ef429 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..43bc0b6 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. @@ -455,21 +460,29 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager @ViewDebug.ExportedProperty(category = "layout") private int mChildCountWithTransientState = 0; + /** + * Currently registered axes for nested scrolling. Flag set consisting of + * {@link #SCROLL_AXIS_HORIZONTAL} {@link #SCROLL_AXIS_VERTICAL} or {@link #SCROLL_AXIS_NONE} + * for null. + */ + private int mNestedScrollAxes; + 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 +512,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 +560,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 +615,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 +633,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 +829,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; } } @@ -2000,6 +2018,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager clearTouchTargets(); resetCancelNextUpFlag(this); mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; + mNestedScrollAxes = SCROLL_AXIS_NONE; } /** @@ -2277,6 +2296,41 @@ 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 + * Activity Transitions. + * @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. + * @see android.app.ActivityOptions#makeSceneTransitionAnimation(android.view.Window, + * android.app.ActivityOptions.ActivityTransitionListener) + */ + 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 +2563,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 +2637,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager for (int i = 0; i < count; i++) { children[i].dispatchDetachedFromWindow(); } + clearDisappearingChildren(); super.dispatchDetachedFromWindow(); } @@ -2915,14 +2970,24 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } } - int saveCount = 0; + int clipSaveCount = 0; final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK; + boolean hasClipBounds = mClipBounds != null && !sIgnoreClipBoundsForChildren; + boolean clippingNeeded = clipToPadding || hasClipBounds; + + if (clippingNeeded) { + clipSaveCount = canvas.save(); + } + if (clipToPadding) { - saveCount = canvas.save(); canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop, mScrollX + mRight - mLeft - mPaddingRight, mScrollY + mBottom - mTop - mPaddingBottom); + } + if (hasClipBounds) { + canvas.clipRect(mClipBounds.left, mClipBounds.top, mClipBounds.right, + mClipBounds.bottom); } // We will draw our child's animation, let's reset the flag @@ -2963,8 +3028,8 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager onDebugDraw(canvas); } - if (clipToPadding) { - canvas.restoreToCount(saveCount); + if (clippingNeeded) { + canvas.restoreToCount(clipSaveCount); } // mGroupFlags might have been updated by drawChild() @@ -3103,7 +3168,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 +3193,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 +3683,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 +3891,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 +3926,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 +3986,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } if (view == focused) { - view.unFocus(); + view.unFocus(null); clearChildFocus = true; } @@ -4008,7 +4073,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } if (view == focused) { - view.unFocus(); + view.unFocus(null); clearChildFocus = true; } @@ -4404,7 +4469,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 +4492,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 +4514,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 +4629,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 +5282,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(); } } @@ -5790,10 +5864,109 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager return true; } + /** + * @inheritDoc + */ + @Override + public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { + return false; + } + + /** + * @inheritDoc + */ + @Override + public void onNestedScrollAccepted(View child, View target, int axes) { + mNestedScrollAxes = axes; + } + + /** + * @inheritDoc + * + * <p>The default implementation of onStopNestedScroll calls + * {@link #stopNestedScroll()} to halt any recursive nested scrolling in progress.</p> + */ + @Override + public void onStopNestedScroll(View child) { + // Stop any recursive nested scrolling. + stopNestedScroll(); + } + + /** + * @inheritDoc + */ + @Override + public void onNestedScroll(View target, int dxConsumed, int dyConsumed, + int dxUnconsumed, int dyUnconsumed) { + // Do nothing + } + + /** + * @inheritDoc + */ + @Override + public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { + // Do nothing + } + + /** + * @inheritDoc + */ + @Override + public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { + return false; + } + + /** + * Return the current axes of nested scrolling for this ViewGroup. + * + * <p>A ViewGroup returning something other than {@link #SCROLL_AXIS_NONE} is currently + * acting as a nested scrolling parent for one or more descendant views in the hierarchy.</p> + * + * @return Flags indicating the current axes of nested scrolling + * @see #SCROLL_AXIS_HORIZONTAL + * @see #SCROLL_AXIS_VERTICAL + * @see #SCROLL_AXIS_NONE + */ + public int getNestedScrollAxes() { + return mNestedScrollAxes; + } + /** @hide */ 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/ViewParent.java b/core/java/android/view/ViewParent.java index 0137693..588b9cd 100644 --- a/core/java/android/view/ViewParent.java +++ b/core/java/android/view/ViewParent.java @@ -407,4 +407,126 @@ public interface ViewParent { * {@link View#TEXT_ALIGNMENT_VIEW_END} */ public int getTextAlignment(); + + /** + * React to a descendant view initiating a nestable scroll operation, claiming the + * nested scroll operation if appropriate. + * + * <p>This method will be called in response to a descendant view invoking + * {@link View#startNestedScroll(int)}. Each parent up the view hierarchy will be + * given an opportunity to respond and claim the nested scrolling operation by returning + * <code>true</code>.</p> + * + * <p>This method may be overridden by ViewParent implementations to indicate when the view + * is willing to support a nested scrolling operation that is about to begin. If it returns + * true, this ViewParent will become the target view's nested scrolling parent for the duration + * of the scroll operation in progress. When the nested scroll is finished this ViewParent + * will receive a call to {@link #onStopNestedScroll(View)}. + * </p> + * + * @param child Direct child of this ViewParent containing target + * @param target View that initiated the nested scroll + * @param nestedScrollAxes Flags consisting of {@link View#SCROLL_AXIS_HORIZONTAL}, + * {@link View#SCROLL_AXIS_VERTICAL} or both + * @return true if this ViewParent accepts the nested scroll operation + */ + public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes); + + /** + * React to the successful claiming of a nested scroll operation. + * + * <p>This method will be called after + * {@link #onStartNestedScroll(View, View, int) onStartNestedScroll} returns true. It offers + * an opportunity for the view and its superclasses to perform initial configuration + * for the nested scroll. Implementations of this method should always call their superclass's + * implementation of this method if one is present.</p> + * + * @param child Direct child of this ViewParent containing target + * @param target View that initiated the nested scroll + * @param nestedScrollAxes Flags consisting of {@link View#SCROLL_AXIS_HORIZONTAL}, + * {@link View#SCROLL_AXIS_VERTICAL} or both + * @see #onStartNestedScroll(View, View, int) + * @see #onStopNestedScroll(View) + */ + public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes); + + /** + * React to a nested scroll operation ending. + * + * <p>Perform cleanup after a nested scrolling operation. + * This method will be called when a nested scroll stops, for example when a nested touch + * scroll ends with a {@link MotionEvent#ACTION_UP} or {@link MotionEvent#ACTION_CANCEL} event. + * Implementations of this method should always call their superclass's implementation of this + * method if one is present.</p> + * + * @param target View that initiated the nested scroll + */ + public void onStopNestedScroll(View target); + + /** + * React to a nested scroll in progress. + * + * <p>This method will be called when the ViewParent's current nested scrolling child view + * dispatches a nested scroll event. To receive calls to this method the ViewParent must have + * previously returned <code>true</code> for a call to + * {@link #onStartNestedScroll(View, View, int)}.</p> + * + * <p>Both the consumed and unconsumed portions of the scroll distance are reported to the + * ViewParent. An implementation may choose to use the consumed portion to match or chase scroll + * position of multiple child elements, for example. The unconsumed portion may be used to + * allow continuous dragging of multiple scrolling or draggable elements, such as scrolling + * a list within a vertical drawer where the drawer begins dragging once the edge of inner + * scrolling content is reached.</p> + * + * @param target The descendent view controlling the nested scroll + * @param dxConsumed Horizontal scroll distance in pixels already consumed by target + * @param dyConsumed Vertical scroll distance in pixels already consumed by target + * @param dxUnconsumed Horizontal scroll distance in pixels not consumed by target + * @param dyUnconsumed Vertical scroll distance in pixels not consumed by target + */ + public void onNestedScroll(View target, int dxConsumed, int dyConsumed, + int dxUnconsumed, int dyUnconsumed); + + /** + * React to a nested scroll in progress before the target view consumes a portion of the scroll. + * + * <p>When working with nested scrolling often the parent view may want an opportunity + * to consume the scroll before the nested scrolling child does. An example of this is a + * drawer that contains a scrollable list. The user will want to be able to scroll the list + * fully into view before the list itself begins scrolling.</p> + * + * <p><code>onNestedPreScroll</code> is called when a nested scrolling child invokes + * {@link View#dispatchNestedPreScroll(int, int, int[], int[])}. The implementation should + * report how any pixels of the scroll reported by dx, dy were consumed in the + * <code>consumed</code> array. Index 0 corresponds to dx and index 1 corresponds to dy. + * This parameter will never be null. Initial values for consumed[0] and consumed[1] + * will always be 0.</p> + * + * @param target View that initiated the nested scroll + * @param dx Horizontal scroll distance in pixels + * @param dy Vertical scroll distance in pixels + * @param consumed Output. The horizontal and vertical scroll distance consumed by this parent + */ + public void onNestedPreScroll(View target, int dx, int dy, int[] consumed); + + /** + * Request a fling from a nested scroll. + * + * <p>This method signifies that a nested scrolling child has detected suitable conditions + * for a fling. Generally this means that a touch scroll has ended with a + * {@link VelocityTracker velocity} in the direction of scrolling that meets or exceeds + * the {@link ViewConfiguration#getScaledMinimumFlingVelocity() minimum fling velocity} + * along a scrollable axis.</p> + * + * <p>If a nested scrolling child view would normally fling but it is at the edge of + * its own content, it can use this method to delegate the fling to its nested scrolling + * parent instead. The parent may optionally consume the fling or observe a child fling.</p> + * + * @param target View that initiated the nested scroll + * @param velocityX Horizontal velocity in pixels per second. + * @param velocityY Vertical velocity in pixels per second + * @param consumed true if the child consumed the fling, false otherwise + * @return true if this parent consumed or otherwise reacted to the fling + */ + public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed); } diff --git a/core/java/android/view/ViewPropertyAnimator.java b/core/java/android/view/ViewPropertyAnimator.java index 67a94be..11d2622 100644 --- a/core/java/android/view/ViewPropertyAnimator.java +++ b/core/java/android/view/ViewPropertyAnimator.java @@ -133,20 +133,22 @@ public class ViewPropertyAnimator { * Constants used to associate a property being requested and the mechanism used to set * the property (this class calls directly into View to set the properties in question). */ - 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; + static final int NONE = 0x0000; + static final int TRANSLATION_X = 0x0001; + static final int TRANSLATION_Y = 0x0002; + static final int TRANSLATION_Z = 0x0004; + static final int SCALE_X = 0x0008; + static final int SCALE_Y = 0x0010; + static final int ROTATION = 0x0020; + static final int ROTATION_X = 0x0040; + static final int ROTATION_Y = 0x0080; + static final int X = 0x0100; + static final int Y = 0x0200; + static final int Z = 0x0400; + static final int ALPHA = 0x0800; + + private static final int TRANSFORM_MASK = TRANSLATION_X | TRANSLATION_Y | TRANSLATION_Z | + SCALE_X | SCALE_Y | ROTATION | ROTATION_X | ROTATION_Y | X | Y | Z; /** * The mechanism by which the user can request several properties that are then animated @@ -469,6 +471,32 @@ public class ViewPropertyAnimator { } /** + * This method will cause the View's <code>z</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#setZ(float) + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimator z(float value) { + animateProperty(Z, value); + return this; + } + + /** + * This method will cause the View's <code>z</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#setZ(float) + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimator zBy(float value) { + animatePropertyBy(Z, value); + return this; + } + + /** * This method will cause the View's <code>rotation</code> property to be animated to the * specified value. Animations already running on the property will be canceled. * @@ -599,6 +627,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 +952,44 @@ 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 Z: + renderNode.setTranslationZ(value - renderNode.getElevation()); break; case ALPHA: info.mAlpha = value; - if (displayList != null) displayList.setAlpha(value); + renderNode.setAlpha(value); break; } } @@ -951,28 +1001,32 @@ 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 Z: + return node.getElevation() + node.getTranslationZ(); case ALPHA: - return info.mAlpha; + return mView.mTransformationInfo.mAlpha; } return 0; } @@ -1061,7 +1115,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 +1123,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 +1145,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 1cb0473..db87394 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 @@ -234,6 +234,7 @@ public final class ViewRootImpl implements ViewParent, InputStage mFirstInputStage; InputStage mFirstPostImeInputStage; + InputStage mSyntheticInputStage; boolean mWindowAttributesChanged = false; int mWindowAttributesChangesFlag = 0; @@ -270,6 +271,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 +298,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)} */ @@ -597,8 +600,8 @@ public final class ViewRootImpl implements ViewParent, // Set up the input pipeline. CharSequence counterSuffix = attrs.getTitle(); - InputStage syntheticInputStage = new SyntheticInputStage(); - InputStage viewPostImeStage = new ViewPostImeInputStage(syntheticInputStage); + mSyntheticInputStage = new SyntheticInputStage(); + InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage); InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage, "aq:native-post-ime:" + counterSuffix); InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage); @@ -621,7 +624,6 @@ public final class ViewRootImpl implements ViewParent, } void destroyHardwareResources() { - invalidateDisplayLists(); if (mAttachInfo.mHardwareRenderer != null) { mAttachInfo.mHardwareRenderer.destroyHardwareResources(mView); mAttachInfo.mHardwareRenderer.destroy(false); @@ -635,23 +637,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,20 +665,28 @@ 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); } } + public boolean invokeFunctor(long functor, boolean waitForCompletion) { + if (mAttachInfo.mHardwareRenderer == null || !mAttachInfo.mHardwareRenderer.isEnabled()) { + return false; + } + mAttachInfo.mHardwareRenderer.invokeFunctor(functor, waitForCompletion); + return true; + } + private void enableHardwareAcceleration(WindowManager.LayoutParams attrs) { mAttachInfo.mHardwareAccelerated = false; mAttachInfo.mHardwareAccelerationRequested = false; @@ -720,7 +732,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 +972,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 +1158,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); + } + void dispatchApplyInsets(View host) { mFitSystemWindowsInsets.set(mAttachInfo.mContentInsets); boolean isRound = false; @@ -1234,11 +1263,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 @@ -1447,6 +1471,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 && @@ -1481,24 +1511,19 @@ 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; + if (mResizeBuffer == null) { + mResizeBuffer = mAttachInfo.mHardwareRenderer.createDisplayListLayer( + mWidth, mHeight); + } + mResizeBuffer.prepare(mWidth, mHeight, false); + RenderNode layerRenderNode = mResizeBuffer.startRecording(); + HardwareCanvas layerCanvas = layerRenderNode.start(mWidth, mHeight); 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; @@ -1516,10 +1541,10 @@ public final class ViewRootImpl implements ViewParent, mTranslator.translateCanvas(layerCanvas); } - DisplayList displayList = mView.mDisplayList; - if (displayList != null && displayList.isValid()) { - layerCanvas.drawDisplayList(displayList, null, - DisplayList.FLAG_CLIP_CHILDREN); + RenderNode renderNode = mView.mRenderNode; + if (renderNode != null && renderNode.isValid()) { + layerCanvas.drawDisplayList(renderNode, null, + RenderNode.FLAG_CLIP_CHILDREN); } else { mView.draw(layerCanvas); } @@ -1529,19 +1554,16 @@ public final class ViewRootImpl implements ViewParent, 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); + layerRenderNode.end(layerCanvas); + layerRenderNode.setCaching(true); + layerRenderNode.setLeftTopRightBottom(0, 0, mWidth, mHeight); + mTempRect.set(0, 0, mWidth, mHeight); } finally { - if (mResizeBuffer != null) { - mResizeBuffer.end(hwRendererCanvas); - if (!completed) { - disposeResizeBuffer(); - } - } + mResizeBuffer.endRecording(mTempRect); } + mAttachInfo.mHardwareRenderer.flushLayerUpdates(); } mAttachInfo.mContentInsets.set(mPendingContentInsets); if (DEBUG_LAYOUT) Log.v(TAG, "Content insets changing to: " @@ -1584,7 +1606,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; @@ -1611,7 +1633,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; @@ -1694,7 +1716,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; } } @@ -2211,11 +2233,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(); } } @@ -2294,6 +2314,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()); @@ -2400,8 +2423,6 @@ public final class ViewRootImpl implements ViewParent, appScale + ", width=" + mWidth + ", height=" + mHeight); } - invalidateDisplayLists(); - attachInfo.mTreeObserver.dispatchOnDraw(); if (!dirty.isEmpty() || mIsAnimating) { @@ -2414,6 +2435,7 @@ public final class ViewRootImpl implements ViewParent, mCurrentDirty.set(dirty); dirty.setEmpty(); + mBlockResizeBuffer = false; attachInfo.mHardwareRenderer.draw(mView, attachInfo, this, animating ? null : mCurrentDirty); } else { @@ -2431,7 +2453,7 @@ public final class ViewRootImpl implements ViewParent, try { attachInfo.mHardwareRenderer.initializeIfNeeded(mWidth, mHeight, - mHolder.getSurface()); + mSurface); } catch (OutOfResourcesException e) { handleOutOfResourcesException(e); return; @@ -2566,28 +2588,31 @@ 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); + 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); @@ -2603,7 +2628,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; @@ -2611,20 +2636,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 */ @@ -2866,10 +2877,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(); } @@ -2886,7 +2893,6 @@ public final class ViewRootImpl implements ViewParent, mView.assignParent(null); mView = null; mAttachInfo.mRootView = null; - mAttachInfo.mSurface = null; mSurface.release(); @@ -2951,7 +2957,7 @@ public final class ViewRootImpl implements ViewParent, } } } - + /** * Return true if child is an ancestor of parent, (or equal to the parent). */ @@ -2998,6 +3004,7 @@ public final class ViewRootImpl implements ViewParent, private final static int MSG_INVALIDATE_WORLD = 23; private final static int MSG_WINDOW_MOVED = 24; private final static int MSG_FLUSH_LAYER_UPDATES = 25; + private final static int MSG_SYNTHESIZE_INPUT_EVENT = 26; final class ViewRootHandler extends Handler { @Override @@ -3047,6 +3054,8 @@ public final class ViewRootImpl implements ViewParent, return "MSG_WINDOW_MOVED"; case MSG_FLUSH_LAYER_UPDATES: return "MSG_FLUSH_LAYER_UPDATES"; + case MSG_SYNTHESIZE_INPUT_EVENT: + return "MSG_SYNTHESIZE_INPUT_EVENT"; } return super.getMessageName(message); } @@ -3142,7 +3151,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 { @@ -3191,8 +3200,6 @@ public final class ViewRootImpl implements ViewParent, mHasHadWindowFocus = true; } - setAccessibilityFocus(null, null); - if (mView != null && mAccessibilityManager.isEnabled()) { if (hasWindowFocus) { mView.sendAccessibilityEvent( @@ -3205,8 +3212,15 @@ public final class ViewRootImpl implements ViewParent, doDie(); break; case MSG_DISPATCH_INPUT_EVENT: { + SomeArgs args = (SomeArgs)msg.obj; + InputEvent event = (InputEvent)args.arg1; + InputEventReceiver receiver = (InputEventReceiver)args.arg2; + enqueueInputEvent(event, receiver, 0, true); + args.recycle(); + } break; + case MSG_SYNTHESIZE_INPUT_EVENT: { InputEvent event = (InputEvent)msg.obj; - enqueueInputEvent(event, null, 0, true); + enqueueInputEvent(event, null, QueuedInputEvent.FLAG_UNHANDLED, true); } break; case MSG_DISPATCH_KEY_FROM_IME: { if (LOCAL_LOGV) Log.v( @@ -3217,7 +3231,8 @@ public final class ViewRootImpl implements ViewParent, // The IME is trying to say this event is from the // system! Bad bad bad! //noinspection UnusedAssignment - event = KeyEvent.changeFlags(event, event.getFlags() & ~KeyEvent.FLAG_FROM_SYSTEM); + event = KeyEvent.changeFlags(event, event.getFlags() & + ~KeyEvent.FLAG_FROM_SYSTEM); } enqueueInputEvent(event, null, QueuedInputEvent.FLAG_DELIVER_POST_IME, true); } break; @@ -3335,7 +3350,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; } } @@ -4013,6 +4028,7 @@ public final class ViewRootImpl implements ViewParent, private final SyntheticJoystickHandler mJoystick = new SyntheticJoystickHandler(); private final SyntheticTouchNavigationHandler mTouchNavigation = new SyntheticTouchNavigationHandler(); + private final SyntheticKeyboardHandler mKeyboard = new SyntheticKeyboardHandler(); public SyntheticInputStage() { super(null); @@ -4035,7 +4051,11 @@ public final class ViewRootImpl implements ViewParent, mTouchNavigation.process(event); return FINISH_HANDLED; } + } else if ((q.mFlags & QueuedInputEvent.FLAG_UNHANDLED) != 0) { + mKeyboard.process((KeyEvent)q.mEvent); + return FINISH_HANDLED; } + return FORWARD; } @@ -4540,8 +4560,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; @@ -4569,9 +4588,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); } @@ -4638,7 +4654,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; @@ -4877,6 +4892,33 @@ public final class ViewRootImpl implements ViewParent, }; } + final class SyntheticKeyboardHandler { + public void process(KeyEvent event) { + if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) != 0) { + return; + } + + final KeyCharacterMap kcm = event.getKeyCharacterMap(); + final int keyCode = event.getKeyCode(); + final int metaState = event.getMetaState(); + + // Check for fallback actions specified by the key character map. + KeyCharacterMap.FallbackAction fallbackAction = + kcm.getFallbackAction(keyCode, metaState); + if (fallbackAction != null) { + final int flags = event.getFlags() | KeyEvent.FLAG_FALLBACK; + KeyEvent fallbackEvent = KeyEvent.obtain( + event.getDownTime(), event.getEventTime(), + event.getAction(), fallbackAction.keyCode, + event.getRepeatCount(), fallbackAction.metaState, + event.getDeviceId(), event.getScanCode(), + flags, event.getSource(), null); + fallbackAction.recycle(); + enqueueInputEvent(fallbackEvent); + } + } + } + /** * Returns true if the key is used for keyboard navigation. * @param keyEvent The key event. @@ -5268,10 +5310,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) { @@ -5319,7 +5361,6 @@ public final class ViewRootImpl implements ViewParent, } if (mAdded && !mFirst) { - invalidateDisplayLists(); destroyHardwareRenderer(); if (mView != null) { @@ -5365,7 +5406,7 @@ public final class ViewRootImpl implements ViewParent, // Hardware rendering if (mAttachInfo.mHardwareRenderer != null) { - if (mAttachInfo.mHardwareRenderer.loadSystemProperties(mHolder.getSurface())) { + if (mAttachInfo.mHardwareRenderer.loadSystemProperties()) { invalidate(); } } @@ -5457,6 +5498,7 @@ public final class ViewRootImpl implements ViewParent, public static final int FLAG_FINISHED = 1 << 2; public static final int FLAG_FINISHED_HANDLED = 1 << 3; public static final int FLAG_RESYNTHESIZED = 1 << 4; + public static final int FLAG_UNHANDLED = 1 << 5; public QueuedInputEvent mNext; @@ -5471,6 +5513,14 @@ public final class ViewRootImpl implements ViewParent, return mEvent instanceof MotionEvent && mEvent.isFromSource(InputDevice.SOURCE_CLASS_POINTER); } + + public boolean shouldSendToSynthesizer() { + if ((mFlags & FLAG_UNHANDLED) != 0) { + return true; + } + + return false; + } } private QueuedInputEvent obtainQueuedInputEvent(InputEvent event, @@ -5568,24 +5618,29 @@ 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; + if (q.shouldSendToSynthesizer()) { + stage = mSyntheticInputStage; + } else { + 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); @@ -5786,10 +5841,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 @@ -5799,7 +5850,20 @@ public final class ViewRootImpl implements ViewParent, } public void dispatchInputEvent(InputEvent event) { - Message msg = mHandler.obtainMessage(MSG_DISPATCH_INPUT_EVENT, event); + dispatchInputEvent(event, null); + } + + public void dispatchInputEvent(InputEvent event, InputEventReceiver receiver) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = event; + args.arg2 = receiver; + Message msg = mHandler.obtainMessage(MSG_DISPATCH_INPUT_EVENT, args); + msg.setAsynchronous(true); + mHandler.sendMessage(msg); + } + + public void synthesizeInputEvent(InputEvent event) { + Message msg = mHandler.obtainMessage(MSG_SYNTHESIZE_INPUT_EVENT, event); msg.setAsynchronous(true); mHandler.sendMessage(msg); } @@ -5810,28 +5874,17 @@ public final class ViewRootImpl implements ViewParent, mHandler.sendMessage(msg); } - public void dispatchUnhandledKey(KeyEvent event) { - if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) { - final KeyCharacterMap kcm = event.getKeyCharacterMap(); - final int keyCode = event.getKeyCode(); - final int metaState = event.getMetaState(); - - // Check for fallback actions specified by the key character map. - KeyCharacterMap.FallbackAction fallbackAction = - kcm.getFallbackAction(keyCode, metaState); - if (fallbackAction != null) { - final int flags = event.getFlags() | KeyEvent.FLAG_FALLBACK; - KeyEvent fallbackEvent = KeyEvent.obtain( - event.getDownTime(), event.getEventTime(), - event.getAction(), fallbackAction.keyCode, - event.getRepeatCount(), fallbackAction.metaState, - event.getDeviceId(), event.getScanCode(), - flags, event.getSource(), null); - fallbackAction.recycle(); - - dispatchInputEvent(fallbackEvent); - } + /** + * Reinject unhandled {@link InputEvent}s in order to synthesize fallbacks events. + * + * Note that it is the responsibility of the caller of this API to recycle the InputEvent it + * passes in. + */ + public void dispatchUnhandledInputEvent(InputEvent event) { + if (event instanceof MotionEvent) { + event = MotionEvent.obtain((MotionEvent) event); } + synthesizeInputEvent(event); } public void dispatchAppVisibility(boolean visible) { @@ -6093,6 +6146,33 @@ public final class ViewRootImpl implements ViewParent, // Do nothing. } + @Override + public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { + return false; + } + + @Override + public void onStopNestedScroll(View target) { + } + + @Override + public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) { + } + + @Override + public void onNestedScroll(View target, int dxConsumed, int dyConsumed, + int dxUnconsumed, int dyUnconsumed) { + } + + @Override + public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { + } + + @Override + public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { + return false; + } + void changeCanvasOpacity(boolean opaque) { // TODO(romainguy): recreate Canvas (software or hardware) to reflect the opacity change. Log.d(TAG, "changeCanvasOpacity: opaque=" + opaque); @@ -6298,68 +6378,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) { @@ -6469,7 +6487,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, @@ -6480,9 +6498,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..9c44bd1 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,6 +27,9 @@ 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; /** @@ -89,17 +94,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 +258,7 @@ public abstract class Window { * * @see #onPreparePanel */ + @Nullable public View onCreatePanelView(int featureId); /** @@ -373,6 +387,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 +995,7 @@ public abstract class Window { * * @return View The current View with focus or null. */ + @Nullable public abstract View getCurrentFocus(); /** @@ -988,10 +1004,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 +1050,7 @@ public abstract class Window { */ public void setBackgroundDrawableResource(int resid) { - setBackgroundDrawable(mContext.getResources().getDrawable(resid)); + setBackgroundDrawable(mContext.getDrawable(resid)); } /** @@ -1328,4 +1346,169 @@ 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; + } + + /** + * Sets the Transition that will be used to move Views into the initial scene. The entering + * Views will be those that are regular Views or ViewGroups that have + * {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend + * {@link android.transition.Visibility} as entering is governed by changing visibility from + * {@link View#INVISIBLE} to {@link View#VISIBLE}. If <code>transition</code> is null, + * entering Views will remain unaffected. + * @param transition The Transition to use to move Views into the initial Scene. + */ + public void setEnterTransition(Transition transition) {} + + /** + * Sets the Transition that will be used to move Views out of the scene when starting a + * new Activity. The exiting Views will be those that are regular Views or ViewGroups that + * have {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend + * {@link android.transition.Visibility} as exiting is governed by changing visibility + * from {@link View#VISIBLE} to {@link View#INVISIBLE}. If transition is null, the views will + * remain unaffected. Requires {@link #FEATURE_CONTENT_TRANSITIONS}. + * @param transition The Transition to use to move Views out of the scene when calling a + * new Activity. + */ + public void setExitTransition(Transition transition) {} + + /** + * Returns the transition used to move Views into the initial scene. The entering + * Views will be those that are regular Views or ViewGroups that have + * {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend + * {@link android.transition.Visibility} as entering is governed by changing visibility from + * {@link View#INVISIBLE} to {@link View#VISIBLE}. If <code>transition</code> is null, + * entering Views will remain unaffected. Requires {@link #FEATURE_CONTENT_TRANSITIONS}. + * + * @return the Transition to use to move Views into the initial Scene. + */ + public Transition getEnterTransition() { return null; } + + /** + * Returns the Transition that will be used to move Views out of the scene when starting a + * new Activity. The exiting Views will be those that are regular Views or ViewGroups that + * have {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend + * {@link android.transition.Visibility} as exiting is governed by changing visibility + * from {@link View#VISIBLE} to {@link View#INVISIBLE}. If transition is null, the views will + * remain unaffected. Requires {@link #FEATURE_CONTENT_TRANSITIONS}. + * @return the Transition to use to move Views out of the scene when calling a + * new Activity. + */ + public Transition getExitTransition() { return null; } + + /** + * Sets the Transition that will be used for shared elements transferred into the content + * Scene. Typical Transitions will affect size and location, such as + * {@link android.transition.MoveImage} and {@link android.transition.ChangeBounds}. A null + * value will cause transferred shared elements to blink to the final position. + * Requires {@link #FEATURE_CONTENT_TRANSITIONS}. + * @param transition The Transition to use for shared elements transferred into the content + * Scene. + */ + public void setSharedElementEnterTransition(Transition transition) {} + + /** + * Returns the Transition that will be used for shared elements transferred into the content + * Scene. Requires {@link #FEATURE_CONTENT_TRANSITIONS}. + * @return Transition to use for sharend elements transferred into the content Scene. + */ + public Transition getSharedElementEnterTransition() { return null; } + + /** + * Sets the Transition that will be used for shared elements after starting a new Activity + * before the shared elements are transferred to the called Activity. If the shared elements + * must animate during the exit transition, this Transition should be used. Upon completion, + * the shared elements may be transferred to the started Activity. + * Requires {@link #FEATURE_CONTENT_TRANSITIONS}. + * @param transition The Transition to use for shared elements in the launching Window + * prior to transferring to the launched Activity's Window. + */ + public void setSharedElementExitTransition(Transition transition) {} + + /** + * Returns the Transition to use for shared elements in the launching Window prior + * to transferring to the launched Activity's Window. + * Requires {@link #FEATURE_CONTENT_TRANSITIONS}. + * + * @return the Transition to use for shared elements in the launching Window prior + * to transferring to the launched Activity's Window. + */ + public Transition getSharedElementExitTransition() { return null; } + + /** + * Controls how the transition set in + * {@link #setEnterTransition(android.transition.Transition)} overlaps with the exit + * transition of the calling Activity. When true, the transition will start as soon as possible. + * When false, the transition will wait until the remote exiting transition completes before + * starting. + * @param allow true to start the enter transition when possible or false to + * wait until the exiting transition completes. + */ + public void setAllowEnterTransitionOverlap(boolean allow) {} + + /** + * Returns how the transition set in + * {@link #setEnterTransition(android.transition.Transition)} overlaps with the exit + * transition of the calling Activity. When true, the transition will start as soon as possible. + * When false, the transition will wait until the remote exiting transition completes before + * starting. + * @return true when the enter transition should start as soon as possible or false to + * when it should wait until the exiting transition completes. + */ + public boolean getAllowEnterTransitionOverlap() { return true; } + + /** + * Controls how the transition set in + * {@link #setExitTransition(android.transition.Transition)} overlaps with the exit + * transition of the called Activity when reentering after if finishes. When true, + * the transition will start as soon as possible. When false, the transition will wait + * until the called Activity's exiting transition completes before starting. + * @param allow true to start the transition when possible or false to wait until the + * called Activity's exiting transition completes. + */ + public void setAllowExitTransitionOverlap(boolean allow) {} + + /** + * Returns how the transition set in + * {@link #setExitTransition(android.transition.Transition)} overlaps with the exit + * transition of the called Activity when reentering after if finishes. When true, + * the transition will start as soon as possible. When false, the transition will wait + * until the called Activity's exiting transition completes before starting. + * @return true when the transition should start when possible or false when it should wait + * until the called Activity's exiting transition completes. + */ + public boolean getAllowExitTransitionOverlap() { return true; } } 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 ae7cd26..4fde1e4 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 @@ -72,14 +76,7 @@ import java.io.PrintWriter; public interface WindowManagerPolicy { // Policy flags. These flags are also defined in frameworks/base/include/ui/Input.h. public final static int FLAG_WAKE = 0x00000001; - public final static int FLAG_WAKE_DROPPED = 0x00000002; - public final static int FLAG_SHIFT = 0x00000004; - public final static int FLAG_CAPS_LOCK = 0x00000008; - public final static int FLAG_ALT = 0x00000010; - public final static int FLAG_ALT_GR = 0x00000020; - public final static int FLAG_MENU = 0x00000040; - public final static int FLAG_LAUNCHER = 0x00000080; - public final static int FLAG_VIRTUAL = 0x00000100; + public final static int FLAG_VIRTUAL = 0x00000002; public final static int FLAG_INJECTED = 0x01000000; public final static int FLAG_TRUSTED = 0x02000000; @@ -447,6 +444,11 @@ public interface WindowManagerPolicy { /** Screen turned off because of timeout */ public final int OFF_BECAUSE_OF_TIMEOUT = 3; + /** @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; @@ -994,6 +996,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. * @@ -1002,7 +1012,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 @@ -1017,7 +1028,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. @@ -1066,7 +1078,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. @@ -1093,6 +1105,7 @@ public interface WindowManagerPolicy { * @see WindowManagerPolicy#USER_ROTATION_LOCKED * @see WindowManagerPolicy#USER_ROTATION_FREE */ + @UserRotationMode public int getUserRotationMode(); /** @@ -1103,12 +1116,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); @@ -1131,6 +1144,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. * @@ -1166,11 +1184,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..bc2d7ec 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; } @@ -256,7 +259,7 @@ public final class InputMethodInfo implements Parcelable { public InputMethodInfo(String packageName, String className, CharSequence label, String settingsActivity) { this(buildDummyResolveInfo(packageName, className, label), false, settingsActivity, null, - 0, false); + 0, false /* forceDefault */, true /* supportsSwitchingToNextInputMethod */); } /** @@ -266,17 +269,26 @@ public final class InputMethodInfo implements Parcelable { public InputMethodInfo(ResolveInfo ri, boolean isAuxIme, String settingsActivity, List<InputMethodSubtype> subtypes, int isDefaultResId, boolean forceDefault) { + this(ri, isAuxIme, settingsActivity, subtypes, isDefaultResId, + forceDefault, true /* supportsSwitchingToNextInputMethod */); + } + + /** + * Temporary API for creating a built-in input method for test. + * @hide + */ + public InputMethodInfo(ResolveInfo ri, boolean isAuxIme, + String settingsActivity, List<InputMethodSubtype> subtypes, int isDefaultResId, + boolean forceDefault, boolean supportsSwitchingToNextInputMethod) { final ServiceInfo si = ri.serviceInfo; mService = ri; mId = new ComponentName(si.packageName, si.name).flattenToShortString(); mSettingsActivityName = settingsActivity; mIsDefaultResId = isDefaultResId; mIsAuxIme = isAuxIme; - if (subtypes != null) { - mSubtypes.addAll(subtypes); - } + mSubtypes = new InputMethodSubtypeArray(subtypes); mForceDefault = forceDefault; - mSupportsSwitchingToNextInputMethod = true; + mSupportsSwitchingToNextInputMethod = supportsSwitchingToNextInputMethod; } private static ResolveInfo buildDummyResolveInfo(String packageName, String className, @@ -338,7 +350,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 +360,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 +374,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 +386,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 +431,7 @@ public final class InputMethodInfo implements Parcelable { pw.println(prefix + "Service:"); mService.dump(pw, prefix + " "); } - + @Override public String toString() { return "InputMethodInfo{" + mId @@ -430,7 +442,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 +479,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 +491,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..0227873 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; @@ -316,6 +317,10 @@ public final class InputMethodManager { int mCursorSelEnd; int mCursorCandStart; int mCursorCandEnd; + /** + * The buffer to retrieve the view location in screen coordinates in {@link #updateCursor}. + */ + private final int[] mViewTopLeft = new int[2]; // ----------------------------------------------------------- @@ -334,6 +339,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 +356,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 +487,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 +557,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 +736,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 +1417,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 +1487,44 @@ 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) != 0; + } } - + + /** + * Returns true if the current input method wants to receive the cursor rectangle in + * screen coordinates rather than local coordinates in the attached view. + * + * @hide + */ + public boolean usesScreenCoordinatesForCursorLocked() { + // {@link InputMethodService#CURSOR_ANCHOR_MONITOR_MODE_CURSOR_RECT} also means + // that {@link InputMethodService#onUpdateCursor} should provide the cursor rectangle + // in screen coordinates rather than local coordinates. + return (mCursorAnchorMonitorMode & + InputMethodService.CURSOR_ANCHOR_MONITOR_MODE_CURSOR_RECT) != 0; + } + + /** + * 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. */ @@ -1470,15 +1536,18 @@ public final class InputMethodManager { || mCurrentTextBoxAttribute == null || mCurMethod == null) { return; } - mTmpCursorRect.set(left, top, right, bottom); if (!mCursorRect.equals(mTmpCursorRect)) { if (DEBUG) Log.d(TAG, "updateCursor"); try { if (DEBUG) Log.v(TAG, "CURSOR CHANGE: " + mCurMethod); - mCurMethod.updateCursor(mTmpCursorRect); mCursorRect.set(mTmpCursorRect); + if (usesScreenCoordinatesForCursorLocked()) { + view.getLocationOnScreen(mViewTopLeft); + mTmpCursorRect.offset(mViewTopLeft[0], mViewTopLeft[1]); + } + mCurMethod.updateCursor(mTmpCursorRect); } catch (RemoteException e) { Log.w(TAG, "IME died: " + mCurId, e); } @@ -1805,6 +1874,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 +1923,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/WebSettings.java b/core/java/android/webkit/WebSettings.java index 98ef66e..7c32c5b 100644 --- a/core/java/android/webkit/WebSettings.java +++ b/core/java/android/webkit/WebSettings.java @@ -171,6 +171,38 @@ public abstract class WebSettings { } /** + * Used with {@link #setMixedContentMode} + * + * In this mode, the WebView will allow a secure origin to load content from any other origin, + * even if that origin is insecure. This is the least secure mode of operation for the WebView, + * and where possible apps should not set this mode. + */ + public static final int MIXED_CONTENT_ALWAYS_ALLOW = 0; + + /** + * Used with {@link #setMixedContentMode} + * + * In this mode, the WebView will not allow a secure origin to load content from an insecure + * origin. This is the preferred and most secure mode of operation for the WebView and apps are + * strongly advised to use this mode. + */ + public static final int MIXED_CONTENT_NEVER_ALLOW = 1; + + /** + * Used with {@link #setMixedContentMode} + * + * In this mode, the WebView will attempt to be compatible with the approach of a modern web + * browser with regard to mixed content. Some insecure content may be allowed to be loaded by + * a secure origin and other types of content will be blocked. The types of content are allowed + * or blocked may change release to release and are not explicitly defined. + * + * This mode is intended to be used by apps that are not in control of the content that they + * render but desire to operate in a reasonably secure environment. For highest security, apps + * are recommended to use {@link #MIXED_CONTENT_NEVER_ALLOW}. + */ + public static final int MIXED_CONTENT_COMPATIBILITY_MODE = 2; + + /** * Hidden constructor to prevent clients from creating a new settings * instance or deriving the class. * @@ -1403,4 +1435,29 @@ public abstract class WebSettings { public int getCacheMode() { throw new MustOverrideException(); } + + /** + * Configures the WebView's behavior when a secure origin attempts to load a resource from an + * insecure origin. + * + * By default, apps that target {@link android.os.Build.VERSION_CODES#KITKAT} or below default + * to {@link #MIXED_CONTENT_ALWAYS_ALLOW}. Apps targeting + * {@link android.os.Build.VERSION_CODES#L} default to {@link #MIXED_CONTENT_NEVER_ALLOW}. + * + * The preferred and most secure mode of operation for the WebView is + * {@link #MIXED_CONTENT_NEVER_ALLOW} and use of {@link #MIXED_CONTENT_ALWAYS_ALLOW} is + * strongly discouraged. + * + * @param mode The mixed content mode to use. One of {@link #MIXED_CONTENT_NEVER_ALLOW}, + * {@link #MIXED_CONTENT_NEVER_ALLOW} or {@link #MIXED_CONTENT_COMPATIBILITY_MODE}. + */ + public abstract void setMixedContentMode(int mode); + + /** + * Gets the current behavior of the WebView with regard to loading insecure content from a + * secure origin. + * @return The current setting, one of {@link #MIXED_CONTENT_NEVER_ALLOW}, + * {@link #MIXED_CONTENT_NEVER_ALLOW} or {@link #MIXED_CONTENT_COMPATIBILITY_MODE}. + */ + public abstract int getMixedContentMode(); } diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index d53bb74..efb246a 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,19 +462,38 @@ 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 * - * @deprecated Private browsing is no longer supported directly via + * @deprecated Private browsing is no longer supported directly via * WebView and will be removed in a future release. Prefer using * {@link WebSettings}, {@link WebViewDatabase}, {@link CookieManager} * 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"); } @@ -670,7 +701,7 @@ public class WebView extends AbsoluteLayout */ @Deprecated public static void enablePlatformNotifications() { - getFactory().getStatics().setPlatformNotificationsEnabled(true); + // noop } /** @@ -682,7 +713,7 @@ public class WebView extends AbsoluteLayout */ @Deprecated public static void disablePlatformNotifications() { - getFactory().getStatics().setPlatformNotificationsEnabled(false); + // noop } /** @@ -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); + } } /** @@ -1093,9 +1136,18 @@ public class WebView extends AbsoluteLayout } /** + * @deprecated Use {@link #createPrintDocumentAdapter(String)} which requires user + * to provide a print document name. + */ + @Deprecated + public PrintDocumentAdapter createPrintDocumentAdapter() { + checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "createPrintDocumentAdapter"); + return mProvider.createPrintDocumentAdapter("default"); + } + + /** * Creates a PrintDocumentAdapter that provides the content of this Webview for printing. - * Only supported for API levels - * {@link android.os.Build.VERSION_CODES#KITKAT} and above. * * The adapter works by converting the Webview contents to a PDF stream. The Webview cannot * be drawn during the conversion process - any such draws are undefined. It is recommended @@ -1103,11 +1155,14 @@ public class WebView extends AbsoluteLayout * temporarily hide a visible WebView by using a custom PrintDocumentAdapter instance * wrapped around the object returned and observing the onStart and onFinish methods. See * {@link android.print.PrintDocumentAdapter} for more information. + * + * @param documentName The user-facing name of the printed document. See + * {@link android.print.PrintDocumentInfo} */ - public PrintDocumentAdapter createPrintDocumentAdapter() { + public PrintDocumentAdapter createPrintDocumentAdapter(String documentName) { checkThread(); if (DebugFlags.TRACE_API) Log.d(LOGTAG, "createPrintDocumentAdapter"); - return mProvider.createPrintDocumentAdapter(); + return mProvider.createPrintDocumentAdapter(documentName); } /** @@ -1421,6 +1476,24 @@ public class WebView extends AbsoluteLayout } /** + * Clears the client certificate preferences table stored in response + * to proceeding/cancelling client cert requests. Note that webview + * automatically clears these preferences when it receives a + * {@link KeyChain.ACTION_STORAGE_CHANGED} + * + * @param resultCallback A callback to be invoked when client certs are cleared. + * The embedder can pass null if not interested in the callback. + * + * TODO(sgurun) unhide + * @hide + */ + public void clearClientCertPreferences(ValueCallback<Void> resultCallback) { + checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "clearClientCertPreferences"); + mProvider.clearClientCertPreferences(resultCallback); + } + + /** * Gets the WebBackForwardList for this WebView. This contains the * back/forward list for use in querying each item in the history stack. * This is a copy of the private WebBackForwardList so it contains only a @@ -1537,6 +1610,8 @@ public class WebView extends AbsoluteLayout * @return the address, or if no address is found, null */ public static String findAddress(String addr) { + // TODO: Rewrite this in Java so it is not needed to start up chromium + // Could also be deprecated return getFactory().getStatics().findAddress(addr); } @@ -1598,6 +1673,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 +1739,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 +2190,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/WebViewClient.java b/core/java/android/webkit/WebViewClient.java index fb842ff..107ae4f 100644 --- a/core/java/android/webkit/WebViewClient.java +++ b/core/java/android/webkit/WebViewClient.java @@ -19,9 +19,12 @@ package android.webkit; import android.graphics.Bitmap; import android.net.http.SslError; import android.os.Message; +import android.view.InputEvent; import android.view.KeyEvent; import android.view.ViewRootImpl; +import java.security.Principal; + public class WebViewClient { /** @@ -204,7 +207,7 @@ public class WebViewClient { handler.cancel(); } - /** + /** * Notify the host application to handle a SSL client certificate * request. The host application is responsible for showing the UI * if desired and providing the keys. There are three ways to @@ -270,11 +273,43 @@ public class WebViewClient { * * @param view The WebView that is initiating the callback. * @param event The key event. + * @deprecated This method is subsumed by the more generic onUnhandledInputEvent. */ + @Deprecated public void onUnhandledKeyEvent(WebView view, KeyEvent event) { + onUnhandledInputEventInternal(view, event); + } + + /** + * Notify the host application that a input event was not handled by the WebView. + * Except system keys, WebView always consumes input events in the normal flow + * or if shouldOverrideKeyEvent returns true. This is called asynchronously + * from where the event is dispatched. It gives the host application a chance + * to handle the unhandled input events. + * + * Note that if the event is a {@link android.view.MotionEvent}, then it's lifetime is only + * that of the function call. If the WebViewClient wishes to use the event beyond that, then it + * <i>must</i> create a copy of the event. + * + * It is the responsibility of overriders of this method to call + * {@link #onUnhandledKeyEvent(WebView, KeyEvent)} + * when appropriate if they wish to continue receiving events through it. + * + * @param view The WebView that is initiating the callback. + * @param event The input event. + */ + public void onUnhandledInputEvent(WebView view, InputEvent event) { + if (event instanceof KeyEvent) { + onUnhandledKeyEvent(view, (KeyEvent) event); + return; + } + onUnhandledInputEventInternal(view, event); + } + + private void onUnhandledInputEventInternal(WebView view, InputEvent event) { ViewRootImpl root = view.getViewRootImpl(); if (root != null) { - root.dispatchUnhandledKey(event); + root.dispatchUnhandledInputEvent(event); } } 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..efa5497 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; @@ -147,7 +148,7 @@ public interface WebViewProvider { public Picture capturePicture(); - public PrintDocumentAdapter createPrintDocumentAdapter(); + public PrintDocumentAdapter createPrintDocumentAdapter(String documentName); public float getScale(); @@ -197,6 +198,8 @@ public interface WebViewProvider { public void clearSslPreferences(); + public void clearClientCertPreferences(ValueCallback<Void> resultCallback); + public WebBackForwardList copyBackForwardList(); public void setFindListener(WebView.FindListener listener); @@ -245,6 +248,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..0966be3 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; @@ -106,6 +110,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te * @see #setTranscriptMode(int) */ public static final int TRANSCRIPT_MODE_DISABLED = 0; + /** * The list will automatically scroll to the bottom when a data set change * notification is received and only if the last item is already visible @@ -114,6 +119,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te * @see #setTranscriptMode(int) */ public static final int TRANSCRIPT_MODE_NORMAL = 1; + /** * The list will automatically scroll to the bottom, no matter what items * are currently visible. @@ -417,7 +423,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 @@ -539,7 +545,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te /** * The last CheckForTap runnable we posted, if any */ - private Runnable mPendingCheckForTap; + private CheckForTap mPendingCheckForTap; /** * The last CheckForKeyLongPress runnable we posted, if any @@ -581,7 +587,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 +705,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 +790,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 +830,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 +1262,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 +1326,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 +1345,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 +1368,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 +1448,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 +1498,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 +1629,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)); } @@ -2075,7 +2128,9 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); + mInLayout = true; + final int childCount = getChildCount(); if (changed) { for (int i = 0; i < childCount; i++) { @@ -2090,8 +2145,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 +2176,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 +2316,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 +2335,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 +2347,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 +2363,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) { @@ -2416,8 +2491,30 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } } + /** + * Positions the selector in a way that mimics keyboard focus. If the + * selector drawable supports hotspots, this manages the focus hotspot. + */ + void positionSelectorLikeFocus(int position, View sel) { + positionSelector(position, sel); + + final Drawable selector = mSelector; + if (selector != null && selector.supportsHotspots() && position != INVALID_POSITION) { + final Rect bounds = mSelectorRect; + final float x = bounds.exactCenterX(); + final float y = bounds.exactCenterY(); + selector.setHotspot(R.attr.state_focused, x, y); + } + } + void positionSelector(int position, View sel) { if (position != INVALID_POSITION) { + if (mSelectorPosition != position) { + final Drawable selector = mSelector; + if (selector != null && selector.supportsHotspots()) { + selector.clearHotspots(); + } + } mSelectorPosition = position; } @@ -2506,8 +2603,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 +2663,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 +2825,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 +2875,8 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te removeCallbacks(mTouchModeReset); mTouchModeReset.run(); } + + mIsDetaching = false; } @Override @@ -2836,8 +2937,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 +3140,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; } @@ -3110,7 +3211,10 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te return INVALID_ROW_ID; } - final class CheckForTap implements Runnable { + private final class CheckForTap implements Runnable { + float x; + float y; + @Override public void run() { if (mTouchMode == TOUCH_MODE_DOWN) { @@ -3130,7 +3234,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te final boolean longClickable = isLongClickable(); if (mSelector != null) { - Drawable d = mSelector.getCurrent(); + final Drawable d = mSelector.getCurrent(); if (d != null && d instanceof TransitionDrawable) { if (longClickable) { ((TransitionDrawable) d).startTransition(longPressTimeout); @@ -3138,6 +3242,9 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te ((TransitionDrawable) d).resetTransition(); } } + if (d.supportsHotspots()) { + d.setHotspot(R.attr.state_pressed, x, y); + } } if (longClickable) { @@ -3402,7 +3509,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 +3517,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; } @@ -3521,6 +3628,8 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te mPendingCheckForTap = new CheckForTap(); } + mPendingCheckForTap.x = ev.getX(); + mPendingCheckForTap.y = ev.getY(); postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } } @@ -3630,6 +3739,9 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te if (d != null && d instanceof TransitionDrawable) { ((TransitionDrawable) d).resetTransition(); } + if (mSelector.supportsHotspots()) { + mSelector.setHotspot(R.attr.state_pressed, x, ev.getY()); + } } if (mTouchModeReset != null) { removeCallbacks(mTouchModeReset); @@ -3641,7 +3753,10 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te mTouchMode = TOUCH_MODE_REST; child.setPressed(false); setPressed(false); - if (!mDataChanged && isAttachedToWindow()) { + if (mSelector != null && mSelector.supportsHotspots()) { + mSelector.removeHotspot(R.attr.state_pressed); + } + if (!mDataChanged && !mIsDetaching && isAttachedToWindow()) { performClick.run(); } } @@ -3900,7 +4015,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 +4031,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 +4039,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 +4436,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 +4458,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 +4490,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 +4508,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 +4518,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 +5967,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 +6237,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 +6343,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 +6415,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 +6488,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 +6591,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; + } + } + + 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 { + // 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); } - return scrapViews.remove(size - 1); } else { - return null; + 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..225cd6d 100644 --- a/core/java/android/widget/AbsSeekBar.java +++ b/core/java/android/widget/AbsSeekBar.java @@ -29,6 +29,8 @@ import android.view.ViewConfiguration; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; +import com.android.internal.R; + public abstract class AbsSeekBar extends ProgressBar { private Drawable mThumb; private int mThumbOffset; @@ -65,11 +67,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 @@ -285,28 +291,39 @@ public abstract class AbsSeekBar extends ProgressBar { */ private void setThumbPos(int w, Drawable thumb, float scale, int gap) { int available = w - mPaddingLeft - mPaddingRight; - int thumbWidth = thumb.getIntrinsicWidth(); - int thumbHeight = thumb.getIntrinsicHeight(); + final int thumbWidth = thumb.getIntrinsicWidth(); + final int thumbHeight = thumb.getIntrinsicHeight(); available -= thumbWidth; // The extra space for the thumb to move on the track available += mThumbOffset * 2; - int thumbPos = (int) (scale * available + 0.5f); + final int thumbPos = (int) (scale * available + 0.5f); - int topBound, bottomBound; + final int top, bottom; if (gap == Integer.MIN_VALUE) { - Rect oldBounds = thumb.getBounds(); - topBound = oldBounds.top; - bottomBound = oldBounds.bottom; + final Rect oldBounds = thumb.getBounds(); + top = oldBounds.top; + bottom = oldBounds.bottom; } else { - topBound = gap; - bottomBound = gap + thumbHeight; + top = gap; + bottom = gap + thumbHeight; } - - // Canvas will be translated, so 0,0 is where we start drawing + final int left = (isLayoutRtl() && mMirrorForRtl) ? available - thumbPos : thumbPos; - thumb.setBounds(left, topBound, left + thumbWidth, bottomBound); + final int right = left + thumbWidth; + + final Drawable background = getBackground(); + if (background != null && background.supportsHotspots()) { + final Rect bounds = mThumb.getBounds(); + final int offsetX = mPaddingLeft - mThumbOffset; + final int offsetY = mPaddingTop; + background.setHotspotBounds(left + offsetX, bounds.top + offsetY, + right + offsetX, bounds.bottom + offsetY); + } + + // Canvas will be translated, so 0,0 is where we start drawing + thumb.setBounds(left, top, right, bottom); } /** @@ -324,6 +341,7 @@ public abstract class AbsSeekBar extends ProgressBar { @Override protected synchronized void onDraw(Canvas canvas) { super.onDraw(canvas); + if (mThumb != null) { canvas.save(); // Translate the padding. For the x, we need to allow the thumb to @@ -420,10 +438,24 @@ public abstract class AbsSeekBar extends ProgressBar { return true; } + private void setHotspot(int id, float x, float y) { + final Drawable bg = getBackground(); + if (bg != null && bg.supportsHotspots()) { + bg.setHotspot(id, x, y); + } + } + + private void clearHotspot(int id) { + final Drawable bg = getBackground(); + if (bg != null && bg.supportsHotspots()) { + bg.removeHotspot(id); + } + } + private void trackTouchEvent(MotionEvent event) { final int width = getWidth(); final int available = width - mPaddingLeft - mPaddingRight; - int x = (int)event.getX(); + final int x = (int) event.getX(); float scale; float progress = 0; if (isLayoutRtl() && mMirrorForRtl) { @@ -447,7 +479,8 @@ public abstract class AbsSeekBar extends ProgressBar { } final int max = getMax(); progress += scale * max; - + + setHotspot(R.attr.state_pressed, x, (int) event.getY()); setProgress((int) progress, true); } @@ -473,6 +506,7 @@ public abstract class AbsSeekBar extends ProgressBar { * canceled. */ void onStopTrackingTouch() { + clearHotspot(R.attr.state_pressed); mIsDragging = false; } 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/com/android/internal/view/menu/ActionMenuPresenter.java b/core/java/android/widget/ActionMenuPresenter.java index fe1cf72..e4575e5 100644 --- a/core/java/com/android/internal/view/menu/ActionMenuPresenter.java +++ b/core/java/android/widget/ActionMenuPresenter.java @@ -14,15 +14,13 @@ * limitations under the License. */ -package com.android.internal.view.menu; +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.transition.Transition; -import android.transition.TransitionManager; import android.util.SparseBooleanArray; import android.view.ActionProvider; import android.view.Gravity; @@ -32,17 +30,23 @@ import android.view.View; import android.view.View.MeasureSpec; import android.view.ViewGroup; import android.view.accessibility.AccessibilityNodeInfo; -import android.widget.ImageButton; -import android.widget.ListPopupWindow; import android.widget.ListPopupWindow.ForwardingListener; import com.android.internal.transition.ActionBarTransition; import com.android.internal.view.ActionBarPolicy; -import com.android.internal.view.menu.ActionMenuView.ActionMenuChildView; +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 { @@ -70,6 +74,7 @@ public class ActionMenuPresenter extends BaseMenuPresenter private ActionButtonSubmenu mActionButtonPopup; private OpenOverflowRunnable mPostedOpenRunnable; + private ActionMenuPopupCallback mPopupCallback; final PopupPresenterCallback mPopupPresenterCallback = new PopupPresenterCallback(); int mOpenSubMenuId; @@ -173,33 +178,17 @@ public class ActionMenuPresenter extends BaseMenuPresenter } @Override - public void bindItemView(final MenuItemImpl item, MenuView.ItemView itemView) { + 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 (item.hasSubMenu()) { - actionItemView.setOnTouchListener(new ForwardingListener(actionItemView) { - @Override - public ListPopupWindow getPopup() { - return mActionButtonPopup != null ? mActionButtonPopup.getPopup() : null; - } - - @Override - protected boolean onForwardingStarted() { - return onSubMenuSelected((SubMenuBuilder) item.getSubMenu()); - } - - @Override - protected boolean onForwardingStopped() { - return dismissPopupMenus(); - } - }); - } else { - actionItemView.setOnTouchListener(null); + if (mPopupCallback == null) { + mPopupCallback = new ActionMenuPopupCallback(); } + actionItemView.setPopupCallback(mPopupCallback); } @Override @@ -553,6 +542,10 @@ public class ActionMenuPresenter extends BaseMenuPresenter } } + public void setMenuView(ActionMenuView menuView) { + mMenuView = menuView; + } + private static class SavedState implements Parcelable { public int openSubMenuId; @@ -585,7 +578,7 @@ public class ActionMenuPresenter extends BaseMenuPresenter }; } - private class OverflowMenuButton extends ImageButton implements ActionMenuChildView { + private class OverflowMenuButton extends ImageButton implements ActionMenuView.ActionMenuChildView { public OverflowMenuButton(Context context) { super(context, null, com.android.internal.R.attr.actionOverflowButtonStyle); @@ -647,16 +640,6 @@ public class ActionMenuPresenter extends BaseMenuPresenter } @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) { - // Fill available height - heightMeasureSpec = MeasureSpec.makeMeasureSpec( - MeasureSpec.getSize(heightMeasureSpec), MeasureSpec.EXACTLY); - } - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - } - - @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); info.setCanOpenPopup(true); @@ -714,14 +697,14 @@ public class ActionMenuPresenter extends BaseMenuPresenter } } - private class PopupPresenterCallback implements MenuPresenter.Callback { + private class PopupPresenterCallback implements Callback { @Override public boolean onOpenSubMenu(MenuBuilder subMenu) { if (subMenu == null) return false; mOpenSubMenuId = ((SubMenuBuilder) subMenu).getItem().getItemId(); - final MenuPresenter.Callback cb = getCallback(); + final Callback cb = getCallback(); return cb != null ? cb.onOpenSubMenu(subMenu) : false; } @@ -730,7 +713,7 @@ public class ActionMenuPresenter extends BaseMenuPresenter if (menu instanceof SubMenuBuilder) { ((SubMenuBuilder) menu).getRootMenu().close(false); } - final MenuPresenter.Callback cb = getCallback(); + final Callback cb = getCallback(); if (cb != null) { cb.onCloseMenu(menu, allMenusAreClosing); } @@ -753,4 +736,11 @@ public class ActionMenuPresenter extends BaseMenuPresenter mPostedOpenRunnable = null; } } + + private class ActionMenuPopupCallback extends ActionMenuItemView.PopupCallback { + @Override + public ListPopupWindow getPopup() { + return mActionButtonPopup != null ? mActionButtonPopup.getPopup() : null; + } + } } diff --git a/core/java/com/android/internal/view/menu/ActionMenuView.java b/core/java/android/widget/ActionMenuView.java index 16a2031..3975edf 100644 --- a/core/java/com/android/internal/view/menu/ActionMenuView.java +++ b/core/java/android/widget/ActionMenuView.java @@ -13,22 +13,29 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.internal.view.menu; +package android.widget; import android.content.Context; import android.content.res.Configuration; -import android.content.res.TypedArray; 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 android.widget.LinearLayout; -import com.android.internal.R; +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; /** - * @hide + * 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"; @@ -44,8 +51,8 @@ public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvo private int mFormatItemsWidth; private int mMinCellSize; private int mGeneratedItemPadding; - private int mMeasuredExtraWidth; - private int mMaxItemHeight; + + private OnMenuItemClickListener mOnMenuItemClickListener; public ActionMenuView(Context context) { this(context, null); @@ -57,26 +64,13 @@ public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvo final float density = context.getResources().getDisplayMetrics().density; mMinCellSize = (int) (MIN_CELL_SIZE * density); mGeneratedItemPadding = (int) (GENERATED_ITEM_PADDING * density); - - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ActionBar, - R.attr.actionBarStyle, 0); - mMaxItemHeight = a.getDimensionPixelSize(R.styleable.ActionBar_height, 0); - a.recycle(); } + /** @hide */ public void setPresenter(ActionMenuPresenter presenter) { mPresenter = presenter; } - public boolean isExpandedFormat() { - return mFormatItems; - } - - public void setMaxItemHeight(int maxItemHeight) { - mMaxItemHeight = maxItemHeight; - requestLayout(); - } - @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); @@ -88,6 +82,10 @@ public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvo } } + 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. @@ -106,11 +104,11 @@ public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvo mMenu.onItemsChanged(true); } - if (mFormatItems) { + final int childCount = getChildCount(); + if (mFormatItems && childCount > 0) { onMeasureExactFormat(widthMeasureSpec, heightMeasureSpec); } else { // Previous measurement at exact format may have set margins - reset them. - final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); @@ -129,10 +127,8 @@ public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvo final int widthPadding = getPaddingLeft() + getPaddingRight(); final int heightPadding = getPaddingTop() + getPaddingBottom(); - final int itemHeightSpec = heightMode == MeasureSpec.EXACTLY - ? MeasureSpec.makeMeasureSpec(heightSize - heightPadding, MeasureSpec.EXACTLY) - : MeasureSpec.makeMeasureSpec( - Math.min(mMaxItemHeight, heightSize - heightPadding), MeasureSpec.AT_MOST); + final int itemHeightSpec = getChildMeasureSpec(heightMeasureSpec, heightPadding, + ViewGroup.LayoutParams.WRAP_CONTENT); widthSize -= widthPadding; @@ -333,7 +329,6 @@ public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvo } setMeasuredDimension(widthSize, heightSize); - mMeasuredExtraWidth = cellsRemaining * cellSize; } /** @@ -496,10 +491,12 @@ public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvo mPresenter.dismissPopupMenus(); } + /** @hide */ public boolean isOverflowReserved() { return mReserveOverflow; } - + + /** @hide */ public void setOverflowReserved(boolean reserveOverflow) { mReserveOverflow = reserveOverflow; } @@ -536,24 +533,53 @@ public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvo 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) { @@ -575,23 +601,72 @@ public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvo 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) { @@ -612,6 +687,7 @@ public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvo 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..9e17cca 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); } @@ -258,15 +261,13 @@ public abstract class CompoundButton extends Button implements Checkable { @Override protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - final Drawable buttonDrawable = mButtonDrawable; if (buttonDrawable != null) { final int verticalGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK; final int drawableHeight = buttonDrawable.getIntrinsicHeight(); final int drawableWidth = buttonDrawable.getIntrinsicWidth(); - int top = 0; + final int top; switch (verticalGravity) { case Gravity.BOTTOM: top = getHeight() - drawableHeight; @@ -274,12 +275,24 @@ public abstract class CompoundButton extends Button implements Checkable { case Gravity.CENTER_VERTICAL: top = (getHeight() - drawableHeight) / 2; break; + default: + top = 0; } - int bottom = top + drawableHeight; - int left = isLayoutRtl() ? getWidth() - drawableWidth : 0; - int right = isLayoutRtl() ? getWidth() : drawableWidth; + final int bottom = top + drawableHeight; + final int left = isLayoutRtl() ? getWidth() - drawableWidth : 0; + final int right = isLayoutRtl() ? getWidth() : drawableWidth; buttonDrawable.setBounds(left, top, right, bottom); + + final Drawable background = getBackground(); + if (background != null && background.supportsHotspots()) { + background.setHotspotBounds(left, top, right, bottom); + } + } + + super.onDraw(canvas); + + if (buttonDrawable != null) { buttonDrawable.draw(canvas); } } 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..10ec105 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; @@ -1556,7 +1565,7 @@ public class ListPopupWindow { // Ensure that keyboard focus starts from the last touched position. setSelectedPositionInt(position); - positionSelector(position, child); + positionSelectorLikeFocus(position, child); // Refresh the drawable state to reflect the new pressed state, // which will also update the selector state. diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java index 78237c3..eeb8015 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 @@ -2539,7 +2564,7 @@ public class ListView extends AbsListView { if (needToRedraw) { if (selectedView != null) { - positionSelector(selectedPos, selectedView); + positionSelectorLikeFocus(selectedPos, selectedView); mSelectedTop = selectedView.getTop(); } if (!awakenScrollBars()) { @@ -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..0c31496 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); } @@ -251,6 +256,16 @@ public class QuickContactBadge extends ImageView implements OnClickListener { } } + /** + * Assigns the drawable that is to be drawn on top of the assigned contact photo. + * + * @param overlay Drawable to be drawn over the assigned contact photo. Must have a non-zero + * instrinsic width and height. + */ + public void setOverlay(Drawable overlay) { + mOverlay = overlay; + } + private void onContactUriChanged() { setEnabled(isAssigned()); } 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..3e46f68 100644 --- a/core/java/android/widget/ScrollView.java +++ b/core/java/android/widget/ScrollView.java @@ -138,6 +138,12 @@ public class ScrollView extends FrameLayout { private int mActivePointerId = INVALID_POINTER; /** + * Used during scrolling to retrieve the new offset within the window. + */ + private final int[] mScrollOffset = new int[2]; + private final int[] mScrollConsumed = new int[2]; + + /** * The StrictMode "critical time span" objects to catch animation * stutters. Non-null when a time-sensitive animation is * in-flight. Must call finish() on them when done animating. @@ -162,12 +168,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)); @@ -501,7 +511,7 @@ public class ScrollView extends FrameLayout { final int y = (int) ev.getY(pointerIndex); final int yDiff = Math.abs(y - mLastMotionY); - if (yDiff > mTouchSlop) { + if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) { mIsBeingDragged = true; mLastMotionY = y; initVelocityTrackerIfNotExists(); @@ -543,6 +553,7 @@ public class ScrollView extends FrameLayout { if (mIsBeingDragged && mScrollStrictSpan == null) { mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll"); } + startNestedScroll(SCROLL_AXIS_VERTICAL); break; } @@ -555,6 +566,7 @@ public class ScrollView extends FrameLayout { if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) { postInvalidateOnAnimation(); } + stopNestedScroll(); break; case MotionEvent.ACTION_POINTER_UP: onSecondaryPointerUp(ev); @@ -602,6 +614,7 @@ public class ScrollView extends FrameLayout { // Remember where the motion event started mLastMotionY = (int) ev.getY(); mActivePointerId = ev.getPointerId(0); + startNestedScroll(SCROLL_AXIS_VERTICAL); break; } case MotionEvent.ACTION_MOVE: @@ -613,6 +626,9 @@ public class ScrollView extends FrameLayout { final int y = (int) ev.getY(activePointerIndex); int deltaY = mLastMotionY - y; + if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) { + deltaY -= mScrollConsumed[1] + mScrollOffset[1]; + } if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) { final ViewParent parent = getParent(); if (parent != null) { @@ -629,22 +645,25 @@ public class ScrollView extends FrameLayout { // Scroll to follow the motion event mLastMotionY = y; - final int oldX = mScrollX; final int oldY = mScrollY; final int range = getScrollRange(); final int overscrollMode = getOverScrollMode(); - final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS || + boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS || (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); // Calling overScrollBy will call onOverScrolled, which // calls onScrollChanged if applicable. - if (overScrollBy(0, deltaY, 0, mScrollY, - 0, range, 0, mOverscrollDistance, true)) { + if (overScrollBy(0, deltaY, 0, mScrollY, 0, range, 0, mOverscrollDistance, true) + && !hasNestedScrollingParent()) { // Break our velocity if we hit a scroll barrier. mVelocityTracker.clear(); } - if (canOverscroll) { + final int scrolledDeltaY = mScrollY - oldY; + final int unconsumedY = deltaY - scrolledDeltaY; + if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) { + mLastMotionY -= mScrollOffset[1]; + } else if (canOverscroll) { final int pulledToY = oldY + deltaY; if (pulledToY < 0) { mEdgeGlowTop.onPull((float) deltaY / getHeight()); @@ -670,15 +689,11 @@ public class ScrollView extends FrameLayout { velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); - if (getChildCount() > 0) { - if ((Math.abs(initialVelocity) > mMinimumVelocity)) { - fling(-initialVelocity); - } else { - if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, - getScrollRange())) { - postInvalidateOnAnimation(); - } - } + if ((Math.abs(initialVelocity) > mMinimumVelocity)) { + flingWithNestedDispatch(-initialVelocity); + } else if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, + getScrollRange())) { + postInvalidateOnAnimation(); } mActivePointerId = INVALID_POINTER; @@ -1549,6 +1564,15 @@ public class ScrollView extends FrameLayout { } } + private void flingWithNestedDispatch(int velocityY) { + final boolean canFling = (mScrollY > 0 || velocityY > 0) && + (mScrollY < getScrollRange() || velocityY < 0); + dispatchNestedFling(0, velocityY, canFling); + if (canFling) { + fling(velocityY); + } + } + private void endDrag() { mIsBeingDragged = false; @@ -1599,6 +1623,47 @@ public class ScrollView extends FrameLayout { } @Override + public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { + return (nestedScrollAxes & SCROLL_AXIS_VERTICAL) != 0; + } + + @Override + public void onNestedScrollAccepted(View child, View target, int axes) { + super.onNestedScrollAccepted(child, target, axes); + startNestedScroll(SCROLL_AXIS_VERTICAL); + } + + /** + * @inheritDoc + */ + @Override + public void onStopNestedScroll(View target) { + super.onStopNestedScroll(target); + } + + @Override + public void onNestedScroll(View target, int dxConsumed, int dyConsumed, + int dxUnconsumed, int dyUnconsumed) { + final int oldScrollY = mScrollY; + scrollBy(0, dyUnconsumed); + final int myConsumed = mScrollY - oldScrollY; + final int myUnconsumed = dyUnconsumed - myConsumed; + dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null); + } + + /** + * @inheritDoc + */ + @Override + public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { + if (!consumed) { + flingWithNestedDispatch((int) velocityY); + return true; + } + return false; + } + + @Override public void draw(Canvas canvas) { super.draw(canvas); if (mEdgeGlowTop != null) { 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..08af4de 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; @@ -714,49 +773,59 @@ public class Switch extends CompoundButton { @Override protected void onDraw(Canvas canvas) { - super.onDraw(canvas); + final Rect tempRect = mTempRect; + final Drawable trackDrawable = mTrackDrawable; + final Drawable thumbDrawable = mThumbDrawable; // Draw the switch - int switchLeft = mSwitchLeft; - int switchTop = mSwitchTop; - int switchRight = mSwitchRight; - int switchBottom = mSwitchBottom; + final int switchLeft = mSwitchLeft; + final int switchTop = mSwitchTop; + final int switchRight = mSwitchRight; + final int switchBottom = mSwitchBottom; + trackDrawable.setBounds(switchLeft, switchTop, switchRight, switchBottom); + 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; + + // Relies on mTempRect, MUST be called first! + final int thumbPos = getThumbOffset(); + + thumbDrawable.getPadding(tempRect); + int thumbLeft = switchInnerLeft - tempRect.left + thumbPos; + int thumbRight = switchInnerLeft + thumbPos + mThumbWidth + tempRect.right; + thumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom); + + final Drawable background = getBackground(); + if (background != null && background.supportsHotspots()) { + background.setHotspotBounds(thumbLeft, switchTop, thumbRight, switchBottom); + } - mTrackDrawable.setBounds(switchLeft, switchTop, switchRight, switchBottom); - mTrackDrawable.draw(canvas); + super.onDraw(canvas); - canvas.save(); + trackDrawable.draw(canvas); - mTrackDrawable.getPadding(mTempRect); - int switchInnerLeft = switchLeft + mTempRect.left; - int switchInnerTop = switchTop + mTempRect.top; - int switchInnerRight = switchRight - mTempRect.right; - int switchInnerBottom = switchBottom - mTempRect.bottom; + final int saveCount = canvas.save(); canvas.clipRect(switchInnerLeft, switchTop, switchInnerRight, switchBottom); + thumbDrawable.draw(canvas); - mThumbDrawable.getPadding(mTempRect); - final int thumbPos = (int) (mThumbPosition + 0.5f); - int thumbLeft = switchInnerLeft - mTempRect.left + thumbPos; - int thumbRight = switchInnerLeft + thumbPos + mThumbWidth + mTempRect.right; - - mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom); - mThumbDrawable.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 +852,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 +933,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..b91111d 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(); @@ -648,6 +652,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener boolean allCaps = false; int shadowcolor = 0; float dx = 0, dy = 0, r = 0; + boolean elegant = false; final Resources.Theme theme = context.getTheme(); @@ -657,8 +662,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); @@ -724,6 +729,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener case com.android.internal.R.styleable.TextAppearance_shadowRadius: r = appearance.getFloat(attr, 0); break; + + case com.android.internal.R.styleable.TextAppearance_elegantTextHeight: + elegant = appearance.getBoolean(attr, false); + break; } } @@ -751,7 +760,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++) { @@ -1061,6 +1070,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener case com.android.internal.R.styleable.TextView_textAllCaps: allCaps = a.getBoolean(attr, false); break; + + case com.android.internal.R.styleable.TextView_elegantTextHeight: + elegant = a.getBoolean(attr, false); + break; } } a.recycle(); @@ -1241,6 +1254,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener setHighlightColor(textColorHighlight); } setRawTextSize(textSize); + setElegantTextHeight(elegant); if (allCaps) { setTransformationMethod(new AllCapsTransformationMethod(getContext())); @@ -1275,9 +1289,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 +2083,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 +2257,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); } /** @@ -2465,6 +2478,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener setTransformationMethod(new AllCapsTransformationMethod(getContext())); } + if (appearance.hasValue(com.android.internal.R.styleable.TextAppearance_elegantTextHeight)) { + setElegantTextHeight(appearance.getBoolean( + com.android.internal.R.styleable.TextAppearance_elegantTextHeight, false)); + } + appearance.recycle(); } @@ -2612,6 +2630,17 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** + * Set the TextView's elegant height metrics flag. This setting selects font + * variants that have not been compacted to fit Latin-based vertical + * metrics, and also increases top and bottom bounds to provide more space. + * + * @param elegant set the paint's elegant metrics flag. + */ + public void setElegantTextHeight(boolean elegant) { + mTextPaint.setElegantTextHeight(elegant); + } + + /** * Sets the text color for all the states (normal, selected, * focused) to be this color. * @@ -4382,8 +4411,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 +4755,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 +4766,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener resetResolvedDrawables(); if (mEditor != null) mEditor.onDetachedFromWindow(); + + super.onDetachedFromWindowInternal(); } @Override @@ -4811,6 +4841,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 +4860,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 +4868,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 +4876,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 +4884,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 +5836,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 +8521,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; diff --git a/core/java/com/android/internal/app/AlertController.java b/core/java/com/android/internal/app/AlertController.java index fe532b0..4726da7 100644 --- a/core/java/com/android/internal/app/AlertController.java +++ b/core/java/com/android/internal/app/AlertController.java @@ -70,6 +70,8 @@ public class AlertController { private View mView; + private int mViewLayoutResId; + private int mViewSpacingLeft; private int mViewSpacingTop; @@ -100,7 +102,7 @@ public class AlertController { private ScrollView mScrollView; - private int mIconId = -1; + private int mIconId = 0; private Drawable mIcon; @@ -126,16 +128,20 @@ public class AlertController { private Handler mHandler; - View.OnClickListener mButtonHandler = new View.OnClickListener() { + private final View.OnClickListener mButtonHandler = new View.OnClickListener() { + @Override public void onClick(View v) { - Message m = null; + final Message m; if (v == mButtonPositive && mButtonPositiveMessage != null) { m = Message.obtain(mButtonPositiveMessage); } else if (v == mButtonNegative && mButtonNegativeMessage != null) { m = Message.obtain(mButtonNegativeMessage); } else if (v == mButtonNeutral && mButtonNeutralMessage != null) { m = Message.obtain(mButtonNeutralMessage); + } else { + m = null; } + if (m != null) { m.sendToTarget(); } @@ -232,11 +238,6 @@ public class AlertController { public void installContent() { /* We use a custom title so never request a window title */ mWindow.requestFeature(Window.FEATURE_NO_TITLE); - - if (mView == null || !canTextInput(mView)) { - mWindow.setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, - WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); - } mWindow.setContentView(mAlertDialogLayout); setupView(); } @@ -263,10 +264,20 @@ public class AlertController { } /** + * Set the view resource to display in the dialog. + */ + public void setView(int layoutResId) { + mView = null; + mViewLayoutResId = layoutResId; + mViewSpacingSpecified = false; + } + + /** * Set the view to display in the dialog. */ public void setView(View view) { mView = view; + mViewLayoutResId = 0; mViewSpacingSpecified = false; } @@ -276,6 +287,7 @@ public class AlertController { public void setView(View view, int viewSpacingLeft, int viewSpacingTop, int viewSpacingRight, int viewSpacingBottom) { mView = view; + mViewLayoutResId = 0; mViewSpacingSpecified = true; mViewSpacingLeft = viewSpacingLeft; mViewSpacingTop = viewSpacingTop; @@ -325,25 +337,39 @@ public class AlertController { } /** - * Set resId to 0 if you don't want an icon. - * @param resId the resourceId of the drawable to use as the icon or 0 - * if you don't want an icon. + * Specifies the icon to display next to the alert title. + * + * @param resId the resource identifier of the drawable to use as the icon, + * or 0 for no icon */ public void setIcon(int resId) { + mIcon = null; mIconId = resId; + if (mIconView != null) { - if (resId > 0) { + if (resId != 0) { mIconView.setImageResource(mIconId); - } else if (resId == 0) { + } else { mIconView.setVisibility(View.GONE); } } } - + + /** + * Specifies the icon to display next to the alert title. + * + * @param icon the drawable to use as the icon or null for no icon + */ public void setIcon(Drawable icon) { mIcon = icon; - if ((mIconView != null) && (mIcon != null)) { - mIconView.setImageDrawable(icon); + mIconId = 0; + + if (mIconView != null) { + if (icon != null) { + mIconView.setImageDrawable(icon); + } else { + mIconView.setVisibility(View.GONE); + } } } @@ -406,28 +432,44 @@ public class AlertController { mWindow.setCloseOnTouchOutsideIfNotSet(true); } - FrameLayout customPanel = null; + final FrameLayout customPanel = (FrameLayout) mWindow.findViewById(R.id.customPanel); + final View customView; if (mView != null) { - customPanel = (FrameLayout) mWindow.findViewById(R.id.customPanel); - FrameLayout custom = (FrameLayout) mWindow.findViewById(R.id.custom); - custom.addView(mView, new LayoutParams(MATCH_PARENT, MATCH_PARENT)); + customView = mView; + } else if (mViewLayoutResId != 0) { + final LayoutInflater inflater = LayoutInflater.from(mContext); + customView = inflater.inflate(mViewLayoutResId, customPanel, false); + } else { + customView = null; + } + + final boolean hasCustomView = customView != null; + if (!hasCustomView || !canTextInput(customView)) { + mWindow.setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, + WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); + } + + if (hasCustomView) { + final FrameLayout custom = (FrameLayout) mWindow.findViewById(R.id.custom); + custom.addView(customView, new LayoutParams(MATCH_PARENT, MATCH_PARENT)); + if (mViewSpacingSpecified) { - custom.setPadding(mViewSpacingLeft, mViewSpacingTop, mViewSpacingRight, - mViewSpacingBottom); + custom.setPadding( + mViewSpacingLeft, mViewSpacingTop, mViewSpacingRight, mViewSpacingBottom); } + if (mListView != null) { ((LinearLayout.LayoutParams) customPanel.getLayoutParams()).weight = 0; } } else { - mWindow.findViewById(R.id.customPanel).setVisibility(View.GONE); + customPanel.setVisibility(View.GONE); } - - /* Only display the divider if we have a title and a - * custom view or a message. - */ + + // Only display the divider if we have a title and a custom view or a + // message. if (hasTitle) { - View divider = null; - if (mMessage != null || mView != null || mListView != null) { + final View divider; + if (mMessage != null || customView != null || mListView != null) { divider = mWindow.findViewById(R.id.titleDivider); } else { divider = mWindow.findViewById(R.id.titleDividerTop); @@ -437,8 +479,9 @@ public class AlertController { divider.setVisibility(View.VISIBLE); } } - - setBackground(topPanel, contentPanel, customPanel, hasButtons, a, hasTitle, buttonPanel); + + setBackground(a, topPanel, contentPanel, customPanel, buttonPanel, hasTitle, hasCustomView, + hasButtons); a.recycle(); } @@ -456,28 +499,24 @@ public class AlertController { View titleTemplate = mWindow.findViewById(R.id.title_template); titleTemplate.setVisibility(View.GONE); } else { - final boolean hasTextTitle = !TextUtils.isEmpty(mTitle); - mIconView = (ImageView) mWindow.findViewById(R.id.icon); + + final boolean hasTextTitle = !TextUtils.isEmpty(mTitle); if (hasTextTitle) { - /* Display the title if a title is supplied, else hide it */ + // Display the title if a title is supplied, else hide it. mTitleView = (TextView) mWindow.findViewById(R.id.alertTitle); - mTitleView.setText(mTitle); - - /* Do this last so that if the user has supplied any - * icons we use them instead of the default ones. If the - * user has specified 0 then make it disappear. - */ - if (mIconId > 0) { + + // Do this last so that if the user has supplied any icons we + // use them instead of the default ones. If the user has + // specified 0 then make it disappear. + if (mIconId != 0) { mIconView.setImageResource(mIconId); } else if (mIcon != null) { mIconView.setImageDrawable(mIcon); - } else if (mIconId == 0) { - - /* Apply the padding from the icon to ensure the - * title is aligned correctly. - */ + } else { + // Apply the padding from the icon to ensure the title is + // aligned correctly. mTitleView.setPadding(mIconView.getPaddingLeft(), mIconView.getPaddingTop(), mIconView.getPaddingRight(), @@ -485,9 +524,8 @@ public class AlertController { mIconView.setVisibility(View.GONE); } } else { - // Hide the title template - View titleTemplate = mWindow.findViewById(R.id.title_template); + final View titleTemplate = mWindow.findViewById(R.id.title_template); titleTemplate.setVisibility(View.GONE); mIconView.setVisibility(View.GONE); topPanel.setVisibility(View.GONE); @@ -596,76 +634,64 @@ public class AlertController { } } - private void setBackground(LinearLayout topPanel, LinearLayout contentPanel, - View customPanel, boolean hasButtons, TypedArray a, boolean hasTitle, - View buttonPanel) { - - /* Get all the different background required */ - int fullDark = a.getResourceId( - R.styleable.AlertDialog_fullDark, R.drawable.popup_full_dark); - int topDark = a.getResourceId( - R.styleable.AlertDialog_topDark, R.drawable.popup_top_dark); - int centerDark = a.getResourceId( - R.styleable.AlertDialog_centerDark, R.drawable.popup_center_dark); - int bottomDark = a.getResourceId( - R.styleable.AlertDialog_bottomDark, R.drawable.popup_bottom_dark); - int fullBright = a.getResourceId( - R.styleable.AlertDialog_fullBright, R.drawable.popup_full_bright); - int topBright = a.getResourceId( + private void setBackground(TypedArray a, View topPanel, View contentPanel, View customPanel, + View buttonPanel, boolean hasTitle, boolean hasCustomView, boolean hasButtons) { + final int topBright = a.getResourceId( R.styleable.AlertDialog_topBright, R.drawable.popup_top_bright); - int centerBright = a.getResourceId( + final int topDark = a.getResourceId( + R.styleable.AlertDialog_topDark, R.drawable.popup_top_dark); + final int centerBright = a.getResourceId( R.styleable.AlertDialog_centerBright, R.drawable.popup_center_bright); - int bottomBright = a.getResourceId( - R.styleable.AlertDialog_bottomBright, R.drawable.popup_bottom_bright); - int bottomMedium = a.getResourceId( - R.styleable.AlertDialog_bottomMedium, R.drawable.popup_bottom_medium); - - /* - * We now set the background of all of the sections of the alert. + final int centerDark = a.getResourceId( + R.styleable.AlertDialog_centerDark, R.drawable.popup_center_dark); + + /* We now set the background of all of the sections of the alert. * First collect together each section that is being displayed along * with whether it is on a light or dark background, then run through * them setting their backgrounds. This is complicated because we need * to correctly use the full, top, middle, and bottom graphics depending * on how many views they are and where they appear. */ - - View[] views = new View[4]; - boolean[] light = new boolean[4]; + + final View[] views = new View[4]; + final boolean[] light = new boolean[4]; View lastView = null; boolean lastLight = false; - + int pos = 0; if (hasTitle) { views[pos] = topPanel; light[pos] = false; pos++; } - + /* The contentPanel displays either a custom text message or * a ListView. If it's text we should use the dark background * for ListView we should use the light background. If neither * are there the contentPanel will be hidden so set it as null. */ - views[pos] = (contentPanel.getVisibility() == View.GONE) - ? null : contentPanel; + views[pos] = contentPanel.getVisibility() == View.GONE ? null : contentPanel; light[pos] = mListView != null; pos++; - if (customPanel != null) { + + if (hasCustomView) { views[pos] = customPanel; light[pos] = mForceInverseBackground; pos++; } + if (hasButtons) { views[pos] = buttonPanel; light[pos] = true; } - + boolean setView = false; - for (pos=0; pos<views.length; pos++) { - View v = views[pos]; + for (pos = 0; pos < views.length; pos++) { + final View v = views[pos]; if (v == null) { continue; } + if (lastView != null) { if (!setView) { lastView.setBackgroundResource(lastLight ? topBright : topDark); @@ -674,23 +700,34 @@ public class AlertController { } setView = true; } + lastView = v; lastLight = light[pos]; } - + if (lastView != null) { if (setView) { - - /* ListViews will use the Bright background but buttons use - * the Medium background. - */ + final int bottomBright = a.getResourceId( + R.styleable.AlertDialog_bottomBright, R.drawable.popup_bottom_bright); + final int bottomMedium = a.getResourceId( + R.styleable.AlertDialog_bottomMedium, R.drawable.popup_bottom_medium); + final int bottomDark = a.getResourceId( + R.styleable.AlertDialog_bottomDark, R.drawable.popup_bottom_dark); + + // ListViews will use the Bright background, but buttons use the + // Medium background. lastView.setBackgroundResource( lastLight ? (hasButtons ? bottomMedium : bottomBright) : bottomDark); } else { + final int fullBright = a.getResourceId( + R.styleable.AlertDialog_fullBright, R.drawable.popup_full_bright); + final int fullDark = a.getResourceId( + R.styleable.AlertDialog_fullDark, R.drawable.popup_full_dark); + lastView.setBackgroundResource(lastLight ? fullBright : fullDark); } } - + /* TODO: uncomment section below. The logic for this should be if * it's a Contextual menu being displayed AND only a Cancel button * is shown then do this. @@ -715,12 +752,14 @@ public class AlertController { mListView.addFooterView(buttonPanel); */ // } - - if ((mListView != null) && (mAdapter != null)) { - mListView.setAdapter(mAdapter); - if (mCheckedItem > -1) { - mListView.setItemChecked(mCheckedItem, true); - mListView.setSelection(mCheckedItem); + + final ListView listView = mListView; + if (listView != null && mAdapter != null) { + listView.setAdapter(mAdapter); + final int checkedItem = mCheckedItem; + if (checkedItem > -1) { + listView.setItemChecked(checkedItem, true); + listView.setSelection(checkedItem); } } } @@ -736,8 +775,13 @@ public class AlertController { super(context, attrs); } - public RecycleListView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public RecycleListView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public RecycleListView( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); } @Override @@ -769,6 +813,7 @@ public class AlertController { public CharSequence[] mItems; public ListAdapter mAdapter; public DialogInterface.OnClickListener mOnClickListener; + public int mViewLayoutResId; public View mView; public int mViewSpacingLeft; public int mViewSpacingTop; @@ -854,8 +899,10 @@ public class AlertController { } else { dialog.setView(mView); } + } else if (mViewLayoutResId != 0) { + dialog.setView(mViewLayoutResId); } - + /* dialog.setCancelable(mCancelable); dialog.setOnCancelListener(mOnCancelListener); @@ -937,7 +984,8 @@ public class AlertController { if (mOnClickListener != null) { listView.setOnItemClickListener(new OnItemClickListener() { - public void onItemClick(AdapterView parent, View v, int position, long id) { + @Override + public void onItemClick(AdapterView<?> parent, View v, int position, long id) { mOnClickListener.onClick(dialog.mDialogInterface, position); if (!mIsSingleChoice) { dialog.mDialogInterface.dismiss(); @@ -946,7 +994,8 @@ public class AlertController { }); } else if (mOnCheckboxClickListener != null) { listView.setOnItemClickListener(new OnItemClickListener() { - public void onItemClick(AdapterView parent, View v, int position, long id) { + @Override + public void onItemClick(AdapterView<?> parent, View v, int position, long id) { if (mCheckedItems != null) { mCheckedItems[position] = listView.isItemChecked(position); } diff --git a/core/java/com/android/internal/app/HeavyWeightSwitcherActivity.java b/core/java/com/android/internal/app/HeavyWeightSwitcherActivity.java index 3d46cdd..83ad9dc 100644 --- a/core/java/com/android/internal/app/HeavyWeightSwitcherActivity.java +++ b/core/java/com/android/internal/app/HeavyWeightSwitcherActivity.java @@ -32,7 +32,6 @@ import android.util.TypedValue; import android.view.View; import android.view.Window; import android.view.View.OnClickListener; -import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; diff --git a/core/java/com/android/internal/app/IAppOpsService.aidl b/core/java/com/android/internal/app/IAppOpsService.aidl index 16c41f3..cd75010 100644 --- a/core/java/com/android/internal/app/IAppOpsService.aidl +++ b/core/java/com/android/internal/app/IAppOpsService.aidl @@ -36,4 +36,6 @@ interface IAppOpsService { List<AppOpsManager.PackageOps> getOpsForPackage(int uid, String packageName, in int[] ops); void setMode(int code, int uid, String packageName, int mode); void resetAllModes(); + int checkAudioOperation(int code, int stream, int uid, String packageName); + void setAudioRestriction(int code, int stream, int uid, int mode, in String[] exceptionPackages); } diff --git a/core/java/com/android/internal/app/IBatteryStats.aidl b/core/java/com/android/internal/app/IBatteryStats.aidl index 43c4b49..1bb577b 100644 --- a/core/java/com/android/internal/app/IBatteryStats.aidl +++ b/core/java/com/android/internal/app/IBatteryStats.aidl @@ -19,22 +19,40 @@ package com.android.internal.app; import com.android.internal.os.BatteryStatsImpl; import android.os.WorkSource; +import android.telephony.DataConnectionRealTimeInfo; import android.telephony.SignalStrength; interface IBatteryStats { - byte[] getStatistics(); - void noteStartWakelock(int uid, int pid, String name, int type); - void noteStopWakelock(int uid, int pid, String name, int type); - - /* DO NOT CHANGE the position of noteStartSensor without updating - SensorService.cpp */ + // These first methods are also called by native code, so must + // be kept in sync with frameworks/native/include/binder/IBatteryStats.h void noteStartSensor(int uid, int sensor); - - /* DO NOT CHANGE the position of noteStopSensor without updating - SensorService.cpp */ void noteStopSensor(int uid, int sensor); - void noteStartWakelockFromSource(in WorkSource ws, int pid, String name, int type); + // Remaining methods are only used in Java. + byte[] getStatistics(); + + // Return the computed amount of time remaining on battery, in milliseconds. + // Returns -1 if nothing could be computed. + long computeBatteryTimeRemaining(); + + // Return the computed amount of time remaining to fully charge, in milliseconds. + // Returns -1 if nothing could be computed. + long computeChargeTimeRemaining(); + + void addIsolatedUid(int isolatedUid, int appUid); + void removeIsolatedUid(int isolatedUid, int appUid); + + void noteEvent(int code, String name, int uid); + + void noteStartWakelock(int uid, int pid, String name, String historyName, + int type, boolean unimportantForLogging); + void noteStopWakelock(int uid, int pid, String name, int type); + + void noteStartWakelockFromSource(in WorkSource ws, int pid, String name, String historyName, + int type, boolean unimportantForLogging); + void noteChangeWakelockFromSource(in WorkSource ws, int pid, String name, int type, + in WorkSource newWs, int newPid, String newName, + String newHistoryName, int newType, boolean newUnimportantForLogging); void noteStopWakelockFromSource(in WorkSource ws, int pid, String name, int type); void noteVibratorOn(int uid, long durationMillis); @@ -46,6 +64,7 @@ interface IBatteryStats { void noteScreenOff(); void noteInputEvent(); void noteUserActivity(int uid, int event); + void noteMobileRadioPowerState(int powerState, long timestampNs); void notePhoneOn(); void notePhoneOff(); void notePhoneSignalStrength(in SignalStrength signalStrength); @@ -56,8 +75,10 @@ interface IBatteryStats { void noteWifiRunning(in WorkSource ws); void noteWifiRunningChanged(in WorkSource oldWs, in WorkSource newWs); void noteWifiStopped(in WorkSource ws); + void noteWifiState(int wifiState, String accessPoint); void noteBluetoothOn(); void noteBluetoothOff(); + void noteBluetoothState(int bluetoothState); void noteFullWifiLockAcquired(int uid); void noteFullWifiLockReleased(int uid); void noteWifiScanStarted(int uid); diff --git a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl new file mode 100644 index 0000000..3219ddd --- /dev/null +++ b/core/java/com/android/internal/app/IVoiceInteractionManagerService.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 com.android.internal.app; + +import android.content.Intent; +import android.os.Bundle; + +import com.android.internal.app.IVoiceInteractor; +import android.service.voice.IVoiceInteractionService; +import android.service.voice.IVoiceInteractionSession; + +interface IVoiceInteractionManagerService { + void startVoiceActivity(in Intent intent, String resolvedType, IVoiceInteractionService service, + in Bundle sessionArgs); + int deliverNewSession(IBinder token, IVoiceInteractionSession session, + IVoiceInteractor interactor); +} diff --git a/core/java/com/android/internal/app/IVoiceInteractor.aidl b/core/java/com/android/internal/app/IVoiceInteractor.aidl new file mode 100644 index 0000000..737906a --- /dev/null +++ b/core/java/com/android/internal/app/IVoiceInteractor.aidl @@ -0,0 +1,33 @@ +/* + * 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.internal.app; + +import android.os.Bundle; + +import com.android.internal.app.IVoiceInteractorCallback; +import com.android.internal.app.IVoiceInteractorRequest; + +/** + * IPC interface for an application to perform calls through a VoiceInteractor. + */ +interface IVoiceInteractor { + IVoiceInteractorRequest startConfirmation(String callingPackage, + IVoiceInteractorCallback callback, String prompt, in Bundle extras); + IVoiceInteractorRequest startCommand(String callingPackage, + IVoiceInteractorCallback callback, String command, in Bundle extras); + boolean[] supportsCommands(String callingPackage, in String[] commands); +} diff --git a/core/java/com/android/internal/app/IVoiceInteractorCallback.aidl b/core/java/com/android/internal/app/IVoiceInteractorCallback.aidl new file mode 100644 index 0000000..c6f93e1 --- /dev/null +++ b/core/java/com/android/internal/app/IVoiceInteractorCallback.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 com.android.internal.app; + +import android.os.Bundle; + +import com.android.internal.app.IVoiceInteractorRequest; + +/** + * IPC interface for an application to receive callbacks from the voice system. + */ +oneway interface IVoiceInteractorCallback { + void deliverConfirmationResult(IVoiceInteractorRequest request, boolean confirmed, + in Bundle result); + void deliverCommandResult(IVoiceInteractorRequest request, boolean complete, in Bundle result); + void deliverCancel(IVoiceInteractorRequest request); +} diff --git a/core/java/com/android/internal/app/IVoiceInteractorRequest.aidl b/core/java/com/android/internal/app/IVoiceInteractorRequest.aidl new file mode 100644 index 0000000..ce2902d --- /dev/null +++ b/core/java/com/android/internal/app/IVoiceInteractorRequest.aidl @@ -0,0 +1,24 @@ +/* + * 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.internal.app; + +/** + * IPC interface identifying a request from an application calling through an IVoiceInteractor. + */ +interface IVoiceInteractorRequest { + void cancel(); +} diff --git a/core/java/com/android/internal/app/IntentForwarderActivity.java b/core/java/com/android/internal/app/IntentForwarderActivity.java new file mode 100644 index 0000000..2f74372 --- /dev/null +++ b/core/java/com/android/internal/app/IntentForwarderActivity.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 com.android.internal.app; + +import android.app.Activity; +import android.app.AppGlobals; +import android.os.Bundle; +import android.content.Context; +import android.content.Intent; +import android.content.pm.IPackageManager; +import android.content.pm.UserInfo; +import android.os.UserHandle; +import android.os.UserManager; +import android.app.ActivityManagerNative; +import android.os.RemoteException; +import android.util.Slog; +import java.util.List; +import java.util.Set; + + + + +/* + * This is used in conjunction with DevicePolicyManager.setForwardingIntents to enable intents to be + * passed in and out of a managed profile. + */ + +public class IntentForwarderActivity extends Activity { + + public static String TAG = "IntentForwarderActivity"; + + public static String FORWARD_INTENT_TO_USER_OWNER + = "com.android.internal.app.ForwardIntentToUserOwner"; + + public static String FORWARD_INTENT_TO_MANAGED_PROFILE + = "com.android.internal.app.ForwardIntentToManagedProfile"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Intent intentReceived = getIntent(); + + String className = intentReceived.getComponent().getClassName(); + final UserHandle userDest; + + if (className.equals(FORWARD_INTENT_TO_USER_OWNER)) { + userDest = UserHandle.OWNER; + } else if (className.equals(FORWARD_INTENT_TO_MANAGED_PROFILE)) { + userDest = getManagedProfile(); + } else { + Slog.wtf(TAG, IntentForwarderActivity.class.getName() + " cannot be called directly"); + userDest = null; + } + if (userDest == null) { // This covers the case where there is no managed profile. + finish(); + return; + } + Intent newIntent = new Intent(intentReceived); + newIntent.setComponent(null); + newIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT + |Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP); + int callingUserId = getUserId(); + IPackageManager ipm = AppGlobals.getPackageManager(); + String resolvedType = newIntent.resolveTypeIfNeeded(getContentResolver()); + boolean canForward = false; + try { + canForward = ipm.canForwardTo(newIntent, resolvedType, callingUserId, + userDest.getIdentifier()); + } catch (RemoteException e) { + Slog.e(TAG, "PackageManagerService is dead?"); + } + if (canForward) { + startActivityAsUser(newIntent, userDest); + } else { + Slog.wtf(TAG, "the intent: " + newIntent + "cannot be forwarded from user " + + callingUserId + " to user " + userDest.getIdentifier()); + } + finish(); + } + + /** + * Returns the managed profile for this device or null if there is no managed + * profile. + * + * TODO: Remove the assumption that there is only one managed profile + * on the device. + */ + private UserHandle getManagedProfile() { + UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE); + List<UserInfo> relatedUsers = userManager.getProfiles(UserHandle.USER_OWNER); + for (UserInfo userInfo : relatedUsers) { + if (userInfo.isManagedProfile()) return new UserHandle(userInfo.id); + } + Slog.wtf(TAG, FORWARD_INTENT_TO_MANAGED_PROFILE + + " has been called, but there is no managed profile"); + return null; + } +} diff --git a/core/java/com/android/internal/app/LocalePicker.java b/core/java/com/android/internal/app/LocalePicker.java index 043964f..ec2d654 100644 --- a/core/java/com/android/internal/app/LocalePicker.java +++ b/core/java/com/android/internal/app/LocalePicker.java @@ -117,7 +117,7 @@ public class LocalePicker extends ListFragment { /** - TODO: Enable when zz_ZY Pseudolocale is complete * if (!localeList.contains("zz_ZY")) { * localeList.add("zz_ZY"); - * } + * } */ } String[] locales = new String[localeList.size()]; diff --git a/core/java/com/android/internal/app/MediaRouteChooserDialogFragment.java b/core/java/com/android/internal/app/MediaRouteChooserDialogFragment.java index ae362af..237feed 100644 --- a/core/java/com/android/internal/app/MediaRouteChooserDialogFragment.java +++ b/core/java/com/android/internal/app/MediaRouteChooserDialogFragment.java @@ -21,7 +21,6 @@ import android.app.DialogFragment; import android.content.Context; import android.os.Bundle; import android.view.View; -import android.view.View.OnClickListener; /** * Media route chooser dialog fragment. diff --git a/core/java/com/android/internal/app/MediaRouteControllerDialog.java b/core/java/com/android/internal/app/MediaRouteControllerDialog.java index 8fc99c7..b0e0373 100644 --- a/core/java/com/android/internal/app/MediaRouteControllerDialog.java +++ b/core/java/com/android/internal/app/MediaRouteControllerDialog.java @@ -256,13 +256,13 @@ public class MediaRouteControllerDialog extends Dialog { private Drawable getIconDrawable() { if (mRoute.isConnecting()) { if (mMediaRouteConnectingDrawable == null) { - mMediaRouteConnectingDrawable = getContext().getResources().getDrawable( + mMediaRouteConnectingDrawable = getContext().getDrawable( R.drawable.ic_media_route_connecting_holo_dark); } return mMediaRouteConnectingDrawable; } else { if (mMediaRouteOnDrawable == null) { - mMediaRouteOnDrawable = getContext().getResources().getDrawable( + mMediaRouteOnDrawable = getContext().getDrawable( R.drawable.ic_media_route_on_holo_dark); } return mMediaRouteOnDrawable; diff --git a/core/java/com/android/internal/app/PlatLogoActivity.java b/core/java/com/android/internal/app/PlatLogoActivity.java index 40a705c..8cdaf91 100644 --- a/core/java/com/android/internal/app/PlatLogoActivity.java +++ b/core/java/com/android/internal/app/PlatLogoActivity.java @@ -18,7 +18,6 @@ package com.android.internal.app; import android.app.Activity; import android.content.ActivityNotFoundException; -import android.content.Context; import android.content.Intent; import android.graphics.Typeface; import android.provider.Settings; @@ -26,19 +25,15 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.text.method.AllCapsTransformationMethod; -import android.text.method.TransformationMethod; import android.util.DisplayMetrics; import android.view.Gravity; import android.view.View; -import android.view.ViewGroup; import android.view.animation.AccelerateInterpolator; import android.view.animation.AnticipateOvershootInterpolator; import android.view.animation.DecelerateInterpolator; import android.widget.FrameLayout; import android.widget.ImageView; -import android.widget.LinearLayout; import android.widget.TextView; -import android.widget.Toast; public class PlatLogoActivity extends Activity { FrameLayout mContent; diff --git a/core/java/com/android/internal/app/ProcessStats.java b/core/java/com/android/internal/app/ProcessStats.java index a87992a..882bec9 100644 --- a/core/java/com/android/internal/app/ProcessStats.java +++ b/core/java/com/android/internal/app/ProcessStats.java @@ -29,8 +29,11 @@ import android.util.Slog; import android.util.SparseArray; import android.util.TimeUtils; import android.webkit.WebViewFactory; -import com.android.internal.util.ArrayUtils; + +import com.android.internal.util.GrowingArrayUtils; + import dalvik.system.VMRuntime; +import libcore.util.EmptyArray; import java.io.IOException; import java.io.InputStream; @@ -171,7 +174,7 @@ public final class ProcessStats implements Parcelable { static final String CSV_SEP = "\t"; // Current version of the parcel format. - private static final int PARCEL_VERSION = 13; + private static final int PARCEL_VERSION = 14; // In-memory Parcel magic number, used to detect attempts to unmarshall bad data private static final int MAGIC = 0x50535453; @@ -189,7 +192,8 @@ public final class ProcessStats implements Parcelable { public String mTimePeriodStartClockStr; public int mFlags; - public final ProcessMap<PackageState> mPackages = new ProcessMap<PackageState>(); + public final ProcessMap<SparseArray<PackageState>> mPackages + = new ProcessMap<SparseArray<PackageState>>(); public final ProcessMap<ProcessState> mProcesses = new ProcessMap<ProcessState>(); public final long[] mMemFactorDurations = new long[ADJ_COUNT]; @@ -227,40 +231,45 @@ public final class ProcessStats implements Parcelable { } public void add(ProcessStats other) { - ArrayMap<String, SparseArray<PackageState>> pkgMap = other.mPackages.getMap(); + ArrayMap<String, SparseArray<SparseArray<PackageState>>> pkgMap = other.mPackages.getMap(); for (int ip=0; ip<pkgMap.size(); ip++) { - String pkgName = pkgMap.keyAt(ip); - SparseArray<PackageState> uids = pkgMap.valueAt(ip); + final String pkgName = pkgMap.keyAt(ip); + final SparseArray<SparseArray<PackageState>> uids = pkgMap.valueAt(ip); for (int iu=0; iu<uids.size(); iu++) { - int uid = uids.keyAt(iu); - PackageState otherState = uids.valueAt(iu); - final int NPROCS = otherState.mProcesses.size(); - final int NSRVS = otherState.mServices.size(); - for (int iproc=0; iproc<NPROCS; iproc++) { - ProcessState otherProc = otherState.mProcesses.valueAt(iproc); - if (otherProc.mCommonProcess != otherProc) { - if (DEBUG) Slog.d(TAG, "Adding pkg " + pkgName + " uid " + uid - + " proc " + otherProc.mName); - ProcessState thisProc = getProcessStateLocked(pkgName, uid, - otherProc.mName); - if (thisProc.mCommonProcess == thisProc) { - if (DEBUG) Slog.d(TAG, "Existing process is single-package, splitting"); - thisProc.mMultiPackage = true; - long now = SystemClock.uptimeMillis(); - final PackageState pkgState = getPackageStateLocked(pkgName, uid); - thisProc = thisProc.clone(thisProc.mPackage, now); - pkgState.mProcesses.put(thisProc.mName, thisProc); + final int uid = uids.keyAt(iu); + final SparseArray<PackageState> versions = uids.valueAt(iu); + for (int iv=0; iv<versions.size(); iv++) { + final int vers = versions.keyAt(iv); + final PackageState otherState = versions.valueAt(iv); + final int NPROCS = otherState.mProcesses.size(); + final int NSRVS = otherState.mServices.size(); + for (int iproc=0; iproc<NPROCS; iproc++) { + ProcessState otherProc = otherState.mProcesses.valueAt(iproc); + if (otherProc.mCommonProcess != otherProc) { + if (DEBUG) Slog.d(TAG, "Adding pkg " + pkgName + " uid " + uid + + " vers " + vers + " proc " + otherProc.mName); + ProcessState thisProc = getProcessStateLocked(pkgName, uid, vers, + otherProc.mName); + if (thisProc.mCommonProcess == thisProc) { + if (DEBUG) Slog.d(TAG, "Existing process is single-package, splitting"); + thisProc.mMultiPackage = true; + long now = SystemClock.uptimeMillis(); + final PackageState pkgState = getPackageStateLocked(pkgName, uid, + vers); + thisProc = thisProc.clone(thisProc.mPackage, now); + pkgState.mProcesses.put(thisProc.mName, thisProc); + } + thisProc.add(otherProc); } - thisProc.add(otherProc); } - } - for (int isvc=0; isvc<NSRVS; isvc++) { - ServiceState otherSvc = otherState.mServices.valueAt(isvc); - if (DEBUG) Slog.d(TAG, "Adding pkg " + pkgName + " uid " + uid - + " service " + otherSvc.mName); - ServiceState thisSvc = getServiceStateLocked(pkgName, uid, - otherSvc.mProcessName, otherSvc.mName); - thisSvc.add(otherSvc); + for (int isvc=0; isvc<NSRVS; isvc++) { + ServiceState otherSvc = otherState.mServices.valueAt(isvc); + if (DEBUG) Slog.d(TAG, "Adding pkg " + pkgName + " uid " + uid + + " service " + otherSvc.mName); + ServiceState thisSvc = getServiceStateLocked(pkgName, uid, vers, + otherSvc.mProcessName, otherSvc.mName); + thisSvc.add(otherSvc); + } } } } @@ -275,9 +284,11 @@ public final class ProcessStats implements Parcelable { if (DEBUG) Slog.d(TAG, "Adding uid " + uid + " proc " + otherProc.mName); if (thisProc == null) { if (DEBUG) Slog.d(TAG, "Creating new process!"); - thisProc = new ProcessState(this, otherProc.mPackage, uid, otherProc.mName); + thisProc = new ProcessState(this, otherProc.mPackage, uid, otherProc.mVersion, + otherProc.mName); mProcesses.put(otherProc.mName, uid, thisProc); - PackageState thisState = getPackageStateLocked(otherProc.mPackage, uid); + PackageState thisState = getPackageStateLocked(otherProc.mPackage, uid, + otherProc.mVersion); if (!thisState.mProcesses.containsKey(otherProc.mName)) { thisState.mProcesses.put(otherProc.mName, thisProc); } @@ -440,7 +451,7 @@ public final class ProcessStats implements Parcelable { } static void dumpServiceTimeCheckin(PrintWriter pw, String label, String packageName, - int uid, String serviceName, ServiceState svc, int serviceType, int opCount, + int uid, int vers, String serviceName, ServiceState svc, int serviceType, int opCount, int curState, long curStartTime, long now) { if (opCount <= 0) { return; @@ -451,6 +462,8 @@ public final class ProcessStats implements Parcelable { pw.print(","); pw.print(uid); pw.print(","); + pw.print(vers); + pw.print(","); pw.print(serviceName); pw.print(","); pw.print(opCount); @@ -775,7 +788,7 @@ public final class ProcessStats implements Parcelable { static void dumpProcessSummaryLocked(PrintWriter pw, String prefix, ArrayList<ProcessState> procs, int[] screenStates, int[] memStates, int[] procStates, - long now, long totalTime) { + boolean inclUidVers, long now, long totalTime) { for (int i=procs.size()-1; i>=0; i--) { ProcessState proc = procs.get(i); pw.print(prefix); @@ -783,6 +796,8 @@ public final class ProcessStats implements Parcelable { pw.print(proc.mName); pw.print(" / "); UserHandle.formatUid(pw, proc.mUid); + pw.print(" / v"); + pw.print(proc.mVersion); pw.println(":"); dumpProcessSummaryDetails(pw, proc, prefix, " TOTAL: ", screenStates, memStates, procStates, now, totalTime, true); @@ -869,6 +884,8 @@ public final class ProcessStats implements Parcelable { pw.print("process"); pw.print(CSV_SEP); pw.print("uid"); + pw.print(CSV_SEP); + pw.print("vers"); dumpStateHeadersCsv(pw, CSV_SEP, sepScreenStates ? screenStates : null, sepMemStates ? memStates : null, sepProcStates ? procStates : null); @@ -878,6 +895,8 @@ public final class ProcessStats implements Parcelable { pw.print(proc.mName); pw.print(CSV_SEP); UserHandle.formatUid(pw, proc.mUid); + pw.print(CSV_SEP); + pw.print(proc.mVersion); dumpProcessStateCsv(pw, proc, sepScreenStates, screenStates, sepMemStates, memStates, sepProcStates, procStates, now); pw.println(); @@ -979,53 +998,88 @@ public final class ProcessStats implements Parcelable { public void resetSafely() { if (DEBUG) Slog.d(TAG, "Safely resetting state of " + mTimePeriodStartClockStr); resetCommon(); - long now = SystemClock.uptimeMillis(); - ArrayMap<String, SparseArray<ProcessState>> procMap = mProcesses.getMap(); + + // First initialize use count of all common processes. + final long now = SystemClock.uptimeMillis(); + final ArrayMap<String, SparseArray<ProcessState>> procMap = mProcesses.getMap(); for (int ip=procMap.size()-1; ip>=0; ip--) { - SparseArray<ProcessState> uids = procMap.valueAt(ip); + final SparseArray<ProcessState> uids = procMap.valueAt(ip); for (int iu=uids.size()-1; iu>=0; iu--) { - ProcessState ps = uids.valueAt(iu); - if (ps.isInUse()) { - uids.valueAt(iu).resetSafely(now); - } else { - uids.valueAt(iu).makeDead(); + uids.valueAt(iu).mTmpNumInUse = 0; + } + } + + // Next reset or prune all per-package processes, and for the ones that are reset + // track this back to the common processes. + final ArrayMap<String, SparseArray<SparseArray<PackageState>>> pkgMap = mPackages.getMap(); + for (int ip=pkgMap.size()-1; ip>=0; ip--) { + final SparseArray<SparseArray<PackageState>> uids = pkgMap.valueAt(ip); + for (int iu=uids.size()-1; iu>=0; iu--) { + final SparseArray<PackageState> vpkgs = uids.valueAt(iu); + for (int iv=vpkgs.size()-1; iv>=0; iv--) { + final PackageState pkgState = vpkgs.valueAt(iv); + for (int iproc=pkgState.mProcesses.size()-1; iproc>=0; iproc--) { + final ProcessState ps = pkgState.mProcesses.valueAt(iproc); + if (ps.isInUse()) { + ps.resetSafely(now); + ps.mCommonProcess.mTmpNumInUse++; + ps.mCommonProcess.mTmpFoundSubProc = ps; + } else { + pkgState.mProcesses.valueAt(iproc).makeDead(); + pkgState.mProcesses.removeAt(iproc); + } + } + for (int isvc=pkgState.mServices.size()-1; isvc>=0; isvc--) { + final ServiceState ss = pkgState.mServices.valueAt(isvc); + if (ss.isInUse()) { + ss.resetSafely(now); + } else { + pkgState.mServices.removeAt(isvc); + } + } + if (pkgState.mProcesses.size() <= 0 && pkgState.mServices.size() <= 0) { + vpkgs.removeAt(iv); + } + } + if (vpkgs.size() <= 0) { uids.removeAt(iu); } } if (uids.size() <= 0) { - procMap.removeAt(ip); + pkgMap.removeAt(ip); } } - ArrayMap<String, SparseArray<PackageState>> pkgMap = mPackages.getMap(); - for (int ip=pkgMap.size()-1; ip>=0; ip--) { - SparseArray<PackageState> uids = pkgMap.valueAt(ip); + + // Finally prune out any common processes that are no longer in use. + for (int ip=procMap.size()-1; ip>=0; ip--) { + final SparseArray<ProcessState> uids = procMap.valueAt(ip); for (int iu=uids.size()-1; iu>=0; iu--) { - PackageState pkgState = uids.valueAt(iu); - for (int iproc=pkgState.mProcesses.size()-1; iproc>=0; iproc--) { - ProcessState ps = pkgState.mProcesses.valueAt(iproc); - if (ps.isInUse() || ps.mCommonProcess.isInUse()) { - pkgState.mProcesses.valueAt(iproc).resetSafely(now); - } else { - pkgState.mProcesses.valueAt(iproc).makeDead(); - pkgState.mProcesses.removeAt(iproc); - } - } - for (int isvc=pkgState.mServices.size()-1; isvc>=0; isvc--) { - ServiceState ss = pkgState.mServices.valueAt(isvc); - if (ss.isInUse()) { - pkgState.mServices.valueAt(isvc).resetSafely(now); + ProcessState ps = uids.valueAt(iu); + if (ps.isInUse() || ps.mTmpNumInUse > 0) { + // If this is a process for multiple packages, we could at this point + // be back down to one package. In that case, we want to revert back + // to a single shared ProcessState. We can do this by converting the + // current package-specific ProcessState up to the shared ProcessState, + // throwing away the current one we have here (because nobody else is + // using it). + if (!ps.mActive && ps.mMultiPackage && ps.mTmpNumInUse == 1) { + // Here we go... + ps = ps.mTmpFoundSubProc; + ps.mCommonProcess = ps; + uids.setValueAt(iu, ps); } else { - pkgState.mServices.removeAt(isvc); + ps.resetSafely(now); } - } - if (pkgState.mProcesses.size() <= 0 && pkgState.mServices.size() <= 0) { + } else { + ps.makeDead(); uids.removeAt(iu); } } if (uids.size() <= 0) { - pkgMap.removeAt(ip); + procMap.removeAt(ip); } } + mStartTime = now; if (DEBUG) Slog.d(TAG, "State reset; now " + mTimePeriodStartClockStr); } @@ -1193,23 +1247,27 @@ public final class ProcessStats implements Parcelable { uids.valueAt(iu).commitStateTime(now); } } - ArrayMap<String, SparseArray<PackageState>> pkgMap = mPackages.getMap(); + final ArrayMap<String, SparseArray<SparseArray<PackageState>>> pkgMap = mPackages.getMap(); final int NPKG = pkgMap.size(); for (int ip=0; ip<NPKG; ip++) { - SparseArray<PackageState> uids = pkgMap.valueAt(ip); + final SparseArray<SparseArray<PackageState>> uids = pkgMap.valueAt(ip); final int NUID = uids.size(); for (int iu=0; iu<NUID; iu++) { - PackageState pkgState = uids.valueAt(iu); - final int NPROCS = pkgState.mProcesses.size(); - for (int iproc=0; iproc<NPROCS; iproc++) { - ProcessState proc = pkgState.mProcesses.valueAt(iproc); - if (proc.mCommonProcess != proc) { - proc.commitStateTime(now); + final SparseArray<PackageState> vpkgs = uids.valueAt(iu); + final int NVERS = vpkgs.size(); + for (int iv=0; iv<NVERS; iv++) { + PackageState pkgState = vpkgs.valueAt(iv); + final int NPROCS = pkgState.mProcesses.size(); + for (int iproc=0; iproc<NPROCS; iproc++) { + ProcessState proc = pkgState.mProcesses.valueAt(iproc); + if (proc.mCommonProcess != proc) { + proc.commitStateTime(now); + } + } + final int NSRVS = pkgState.mServices.size(); + for (int isvc=0; isvc<NSRVS; isvc++) { + pkgState.mServices.valueAt(isvc).commitStateTime(now); } - } - final int NSRVS = pkgState.mServices.size(); - for (int isvc=0; isvc<NSRVS; isvc++) { - pkgState.mServices.valueAt(isvc).commitStateTime(now); } } } @@ -1239,46 +1297,53 @@ public final class ProcessStats implements Parcelable { out.writeInt(NPROC); for (int ip=0; ip<NPROC; ip++) { writeCommonString(out, procMap.keyAt(ip)); - SparseArray<ProcessState> uids = procMap.valueAt(ip); + final SparseArray<ProcessState> uids = procMap.valueAt(ip); final int NUID = uids.size(); out.writeInt(NUID); for (int iu=0; iu<NUID; iu++) { out.writeInt(uids.keyAt(iu)); - ProcessState proc = uids.valueAt(iu); + final ProcessState proc = uids.valueAt(iu); writeCommonString(out, proc.mPackage); + out.writeInt(proc.mVersion); proc.writeToParcel(out, now); } } out.writeInt(NPKG); for (int ip=0; ip<NPKG; ip++) { writeCommonString(out, pkgMap.keyAt(ip)); - SparseArray<PackageState> uids = pkgMap.valueAt(ip); + final SparseArray<SparseArray<PackageState>> uids = pkgMap.valueAt(ip); final int NUID = uids.size(); out.writeInt(NUID); for (int iu=0; iu<NUID; iu++) { out.writeInt(uids.keyAt(iu)); - PackageState pkgState = uids.valueAt(iu); - final int NPROCS = pkgState.mProcesses.size(); - out.writeInt(NPROCS); - for (int iproc=0; iproc<NPROCS; iproc++) { - writeCommonString(out, pkgState.mProcesses.keyAt(iproc)); - ProcessState proc = pkgState.mProcesses.valueAt(iproc); - if (proc.mCommonProcess == proc) { - // This is the same as the common process we wrote above. - out.writeInt(0); - } else { - // There is separate data for this package's process. - out.writeInt(1); - proc.writeToParcel(out, now); + final SparseArray<PackageState> vpkgs = uids.valueAt(iu); + final int NVERS = vpkgs.size(); + out.writeInt(NVERS); + for (int iv=0; iv<NVERS; iv++) { + out.writeInt(vpkgs.keyAt(iv)); + final PackageState pkgState = vpkgs.valueAt(iv); + final int NPROCS = pkgState.mProcesses.size(); + out.writeInt(NPROCS); + for (int iproc=0; iproc<NPROCS; iproc++) { + writeCommonString(out, pkgState.mProcesses.keyAt(iproc)); + final ProcessState proc = pkgState.mProcesses.valueAt(iproc); + if (proc.mCommonProcess == proc) { + // This is the same as the common process we wrote above. + out.writeInt(0); + } else { + // There is separate data for this package's process. + out.writeInt(1); + proc.writeToParcel(out, now); + } + } + final int NSRVS = pkgState.mServices.size(); + out.writeInt(NSRVS); + for (int isvc=0; isvc<NSRVS; isvc++) { + out.writeString(pkgState.mServices.keyAt(isvc)); + final ServiceState svc = pkgState.mServices.valueAt(isvc); + writeCommonString(out, svc.mProcessName); + svc.writeToParcel(out, now); } - } - final int NSRVS = pkgState.mServices.size(); - out.writeInt(NSRVS); - for (int isvc=0; isvc<NSRVS; isvc++) { - out.writeString(pkgState.mServices.keyAt(isvc)); - ServiceState svc = pkgState.mServices.valueAt(isvc); - writeCommonString(out, svc.mProcessName); - svc.writeToParcel(out, now); } } } @@ -1396,7 +1461,7 @@ public final class ProcessStats implements Parcelable { } while (NPROC > 0) { NPROC--; - String procName = readCommonString(in, version); + final String procName = readCommonString(in, version); if (procName == null) { mReadError = "bad process name"; return; @@ -1408,23 +1473,24 @@ public final class ProcessStats implements Parcelable { } while (NUID > 0) { NUID--; - int uid = in.readInt(); + final int uid = in.readInt(); if (uid < 0) { mReadError = "bad uid: " + uid; return; } - String pkgName = readCommonString(in, version); + final String pkgName = readCommonString(in, version); if (pkgName == null) { mReadError = "bad process package name"; return; } + final int vers = in.readInt(); ProcessState proc = hadData ? mProcesses.get(procName, uid) : null; if (proc != null) { if (!proc.readFromParcel(in, false)) { return; } } else { - proc = new ProcessState(this, pkgName, uid, procName); + proc = new ProcessState(this, pkgName, uid, vers, procName); if (!proc.readFromParcel(in, true)) { return; } @@ -1444,7 +1510,7 @@ public final class ProcessStats implements Parcelable { } while (NPKG > 0) { NPKG--; - String pkgName = readCommonString(in, version); + final String pkgName = readCommonString(in, version); if (pkgName == null) { mReadError = "bad package name"; return; @@ -1456,83 +1522,98 @@ public final class ProcessStats implements Parcelable { } while (NUID > 0) { NUID--; - int uid = in.readInt(); + final int uid = in.readInt(); if (uid < 0) { mReadError = "bad uid: " + uid; return; } - PackageState pkgState = new PackageState(pkgName, uid); - mPackages.put(pkgName, uid, pkgState); - int NPROCS = in.readInt(); - if (NPROCS < 0) { - mReadError = "bad package process count: " + NPROCS; + int NVERS = in.readInt(); + if (NVERS < 0) { + mReadError = "bad versions count: " + NVERS; return; } - while (NPROCS > 0) { - NPROCS--; - String procName = readCommonString(in, version); - if (procName == null) { - mReadError = "bad package process name"; - return; + while (NVERS > 0) { + NVERS--; + final int vers = in.readInt(); + PackageState pkgState = new PackageState(pkgName, uid); + SparseArray<PackageState> vpkg = mPackages.get(pkgName, uid); + if (vpkg == null) { + vpkg = new SparseArray<PackageState>(); + mPackages.put(pkgName, uid, vpkg); } - int hasProc = in.readInt(); - if (DEBUG_PARCEL) Slog.d(TAG, "Reading package " + pkgName + " " + uid - + " process " + procName + " hasProc=" + hasProc); - ProcessState commonProc = mProcesses.get(procName, uid); - if (DEBUG_PARCEL) Slog.d(TAG, "Got common proc " + procName + " " + uid - + ": " + commonProc); - if (commonProc == null) { - mReadError = "no common proc: " + procName; + vpkg.put(vers, pkgState); + int NPROCS = in.readInt(); + if (NPROCS < 0) { + mReadError = "bad package process count: " + NPROCS; return; } - if (hasProc != 0) { - // The process for this package is unique to the package; we - // need to load it. We don't need to do anything about it if - // it is not unique because if someone later looks for it - // they will find and use it from the global procs. - ProcessState proc = hadData ? pkgState.mProcesses.get(procName) : null; - if (proc != null) { - if (!proc.readFromParcel(in, false)) { - return; + while (NPROCS > 0) { + NPROCS--; + String procName = readCommonString(in, version); + if (procName == null) { + mReadError = "bad package process name"; + return; + } + int hasProc = in.readInt(); + if (DEBUG_PARCEL) Slog.d(TAG, "Reading package " + pkgName + " " + uid + + " process " + procName + " hasProc=" + hasProc); + ProcessState commonProc = mProcesses.get(procName, uid); + if (DEBUG_PARCEL) Slog.d(TAG, "Got common proc " + procName + " " + uid + + ": " + commonProc); + if (commonProc == null) { + mReadError = "no common proc: " + procName; + return; + } + if (hasProc != 0) { + // The process for this package is unique to the package; we + // need to load it. We don't need to do anything about it if + // it is not unique because if someone later looks for it + // they will find and use it from the global procs. + ProcessState proc = hadData ? pkgState.mProcesses.get(procName) : null; + if (proc != null) { + if (!proc.readFromParcel(in, false)) { + return; + } + } else { + proc = new ProcessState(commonProc, pkgName, uid, vers, procName, + 0); + if (!proc.readFromParcel(in, true)) { + return; + } } + if (DEBUG_PARCEL) Slog.d(TAG, "Adding package " + pkgName + " process: " + + procName + " " + uid + " " + proc); + pkgState.mProcesses.put(procName, proc); } else { - proc = new ProcessState(commonProc, pkgName, uid, procName, 0); - if (!proc.readFromParcel(in, true)) { - return; - } + if (DEBUG_PARCEL) Slog.d(TAG, "Adding package " + pkgName + " process: " + + procName + " " + uid + " " + commonProc); + pkgState.mProcesses.put(procName, commonProc); } - if (DEBUG_PARCEL) Slog.d(TAG, "Adding package " + pkgName + " process: " - + procName + " " + uid + " " + proc); - pkgState.mProcesses.put(procName, proc); - } else { - if (DEBUG_PARCEL) Slog.d(TAG, "Adding package " + pkgName + " process: " - + procName + " " + uid + " " + commonProc); - pkgState.mProcesses.put(procName, commonProc); } - } - int NSRVS = in.readInt(); - if (NSRVS < 0) { - mReadError = "bad package service count: " + NSRVS; - return; - } - while (NSRVS > 0) { - NSRVS--; - String serviceName = in.readString(); - if (serviceName == null) { - mReadError = "bad package service name"; + int NSRVS = in.readInt(); + if (NSRVS < 0) { + mReadError = "bad package service count: " + NSRVS; return; } - String processName = version > 9 ? readCommonString(in, version) : null; - ServiceState serv = hadData ? pkgState.mServices.get(serviceName) : null; - if (serv == null) { - serv = new ServiceState(this, pkgName, serviceName, processName, null); - } - if (!serv.readFromParcel(in)) { - return; + while (NSRVS > 0) { + NSRVS--; + String serviceName = in.readString(); + if (serviceName == null) { + mReadError = "bad package service name"; + return; + } + String processName = version > 9 ? readCommonString(in, version) : null; + ServiceState serv = hadData ? pkgState.mServices.get(serviceName) : null; + if (serv == null) { + serv = new ServiceState(this, pkgName, serviceName, processName, null); + } + if (!serv.readFromParcel(in)) { + return; + } + if (DEBUG_PARCEL) Slog.d(TAG, "Adding package " + pkgName + " service: " + + serviceName + " " + uid + " " + serv); + pkgState.mServices.put(serviceName, serv); } - if (DEBUG_PARCEL) Slog.d(TAG, "Adding package " + pkgName + " service: " - + serviceName + " " + uid + " " + serv); - pkgState.mServices.put(serviceName, serv); } } } @@ -1543,21 +1624,10 @@ public final class ProcessStats implements Parcelable { } int addLongData(int index, int type, int num) { - int tableLen = mAddLongTable != null ? mAddLongTable.length : 0; - if (mAddLongTableSize >= tableLen) { - int newSize = ArrayUtils.idealIntArraySize(tableLen + 1); - int[] newTable = new int[newSize]; - if (tableLen > 0) { - System.arraycopy(mAddLongTable, 0, newTable, 0, tableLen); - } - mAddLongTable = newTable; - } - if (mAddLongTableSize > 0 && mAddLongTableSize - index != 0) { - System.arraycopy(mAddLongTable, index, mAddLongTable, index + 1, - mAddLongTableSize - index); - } int off = allocLongData(num); - mAddLongTable[index] = type | off; + mAddLongTable = GrowingArrayUtils.insert( + mAddLongTable != null ? mAddLongTable : EmptyArray.INT, + mAddLongTableSize, index, type | off); mAddLongTableSize++; return off; } @@ -1627,30 +1697,36 @@ public final class ProcessStats implements Parcelable { return ~lo; // value not present } - public PackageState getPackageStateLocked(String packageName, int uid) { - PackageState as = mPackages.get(packageName, uid); + public PackageState getPackageStateLocked(String packageName, int uid, int vers) { + SparseArray<PackageState> vpkg = mPackages.get(packageName, uid); + if (vpkg == null) { + vpkg = new SparseArray<PackageState>(); + mPackages.put(packageName, uid, vpkg); + } + PackageState as = vpkg.get(vers); if (as != null) { return as; } as = new PackageState(packageName, uid); - mPackages.put(packageName, uid, as); + vpkg.put(vers, as); return as; } - public ProcessState getProcessStateLocked(String packageName, int uid, String processName) { - final PackageState pkgState = getPackageStateLocked(packageName, uid); + public ProcessState getProcessStateLocked(String packageName, int uid, int vers, + String processName) { + final PackageState pkgState = getPackageStateLocked(packageName, uid, vers); ProcessState ps = pkgState.mProcesses.get(processName); if (ps != null) { return ps; } ProcessState commonProc = mProcesses.get(processName, uid); if (commonProc == null) { - commonProc = new ProcessState(this, packageName, uid, processName); + commonProc = new ProcessState(this, packageName, uid, vers, processName); mProcesses.put(processName, uid, commonProc); if (DEBUG) Slog.d(TAG, "GETPROC created new common " + commonProc); } if (!commonProc.mMultiPackage) { - if (packageName.equals(commonProc.mPackage)) { + if (packageName.equals(commonProc.mPackage) && vers == commonProc.mVersion) { // This common process is not in use by multiple packages, and // is for the calling package, so we can just use it directly. ps = commonProc; @@ -1668,7 +1744,8 @@ public final class ProcessStats implements Parcelable { long now = SystemClock.uptimeMillis(); // First let's make a copy of the current process state and put // that under the now unique state for its original package name. - final PackageState commonPkgState = getPackageStateLocked(commonProc.mPackage, uid); + final PackageState commonPkgState = getPackageStateLocked(commonProc.mPackage, + uid, commonProc.mVersion); if (commonPkgState != null) { ProcessState cloned = commonProc.clone(commonProc.mPackage, now); if (DEBUG) Slog.d(TAG, "GETPROC setting clone to pkg " + commonProc.mPackage @@ -1691,13 +1768,13 @@ public final class ProcessStats implements Parcelable { + "/" + uid + " for proc " + commonProc.mName); } // And now make a fresh new process state for the new package name. - ps = new ProcessState(commonProc, packageName, uid, processName, now); + ps = new ProcessState(commonProc, packageName, uid, vers, processName, now); if (DEBUG) Slog.d(TAG, "GETPROC created new pkg " + ps); } } else { // The common process is for multiple packages, we need to create a // separate object for the per-package data. - ps = new ProcessState(commonProc, packageName, uid, processName, + ps = new ProcessState(commonProc, packageName, uid, vers, processName, SystemClock.uptimeMillis()); if (DEBUG) Slog.d(TAG, "GETPROC created new pkg " + ps); } @@ -1706,16 +1783,16 @@ public final class ProcessStats implements Parcelable { return ps; } - public ProcessStats.ServiceState getServiceStateLocked(String packageName, int uid, + public ProcessStats.ServiceState getServiceStateLocked(String packageName, int uid, int vers, String processName, String className) { - final ProcessStats.PackageState as = getPackageStateLocked(packageName, uid); + final ProcessStats.PackageState as = getPackageStateLocked(packageName, uid, vers); ProcessStats.ServiceState ss = as.mServices.get(className); if (ss != null) { if (DEBUG) Slog.d(TAG, "GETSVC: returning existing " + ss); return ss; } final ProcessStats.ProcessState ps = processName != null - ? getProcessStateLocked(packageName, uid, processName) : null; + ? getProcessStateLocked(packageName, uid, vers, processName) : null; ss = new ProcessStats.ServiceState(this, packageName, className, processName, ps); as.mServices.put(className, ss); if (DEBUG) Slog.d(TAG, "GETSVC: creating " + ss + " in " + ps); @@ -1756,119 +1833,124 @@ public final class ProcessStats implements Parcelable { boolean dumpAll, boolean activeOnly) { long totalTime = dumpSingleTime(null, null, mMemFactorDurations, mMemFactor, mStartTime, now); - ArrayMap<String, SparseArray<PackageState>> pkgMap = mPackages.getMap(); + ArrayMap<String, SparseArray<SparseArray<PackageState>>> pkgMap = mPackages.getMap(); boolean printedHeader = false; boolean sepNeeded = false; for (int ip=0; ip<pkgMap.size(); ip++) { final String pkgName = pkgMap.keyAt(ip); - final SparseArray<PackageState> uids = pkgMap.valueAt(ip); + final SparseArray<SparseArray<PackageState>> uids = pkgMap.valueAt(ip); for (int iu=0; iu<uids.size(); iu++) { final int uid = uids.keyAt(iu); - final PackageState pkgState = uids.valueAt(iu); - final int NPROCS = pkgState.mProcesses.size(); - final int NSRVS = pkgState.mServices.size(); - final boolean pkgMatch = reqPackage == null || reqPackage.equals(pkgName); - if (!pkgMatch) { - boolean procMatch = false; - for (int iproc=0; iproc<NPROCS; iproc++) { - ProcessState proc = pkgState.mProcesses.valueAt(iproc); - if (reqPackage.equals(proc.mName)) { - procMatch = true; - break; + final SparseArray<PackageState> vpkgs = uids.valueAt(iu); + for (int iv=0; iv<vpkgs.size(); iv++) { + final int vers = vpkgs.keyAt(iv); + final PackageState pkgState = vpkgs.valueAt(iv); + final int NPROCS = pkgState.mProcesses.size(); + final int NSRVS = pkgState.mServices.size(); + final boolean pkgMatch = reqPackage == null || reqPackage.equals(pkgName); + if (!pkgMatch) { + boolean procMatch = false; + for (int iproc=0; iproc<NPROCS; iproc++) { + ProcessState proc = pkgState.mProcesses.valueAt(iproc); + if (reqPackage.equals(proc.mName)) { + procMatch = true; + break; + } + } + if (!procMatch) { + continue; } } - if (!procMatch) { - continue; + if (NPROCS > 0 || NSRVS > 0) { + if (!printedHeader) { + pw.println("Per-Package Stats:"); + printedHeader = true; + sepNeeded = true; + } + pw.print(" * "); pw.print(pkgName); pw.print(" / "); + UserHandle.formatUid(pw, uid); pw.print(" / v"); + pw.print(vers); pw.println(":"); } - } - if (NPROCS > 0 || NSRVS > 0) { - if (!printedHeader) { - pw.println("Per-Package Stats:"); - printedHeader = true; - sepNeeded = true; + if (!dumpSummary || dumpAll) { + for (int iproc=0; iproc<NPROCS; iproc++) { + ProcessState proc = pkgState.mProcesses.valueAt(iproc); + if (!pkgMatch && !reqPackage.equals(proc.mName)) { + continue; + } + if (activeOnly && !proc.isInUse()) { + pw.print(" (Not active: "); + pw.print(pkgState.mProcesses.keyAt(iproc)); pw.println(")"); + continue; + } + pw.print(" Process "); + pw.print(pkgState.mProcesses.keyAt(iproc)); + if (proc.mCommonProcess.mMultiPackage) { + pw.print(" (multi, "); + } else { + pw.print(" (unique, "); + } + pw.print(proc.mDurationsTableSize); + pw.print(" entries)"); + pw.println(":"); + dumpProcessState(pw, " ", proc, ALL_SCREEN_ADJ, ALL_MEM_ADJ, + ALL_PROC_STATES, now); + dumpProcessPss(pw, " ", proc, ALL_SCREEN_ADJ, ALL_MEM_ADJ, + ALL_PROC_STATES); + dumpProcessInternalLocked(pw, " ", proc, dumpAll); + } + } else { + ArrayList<ProcessState> procs = new ArrayList<ProcessState>(); + for (int iproc=0; iproc<NPROCS; iproc++) { + ProcessState proc = pkgState.mProcesses.valueAt(iproc); + if (!pkgMatch && !reqPackage.equals(proc.mName)) { + continue; + } + if (activeOnly && !proc.isInUse()) { + continue; + } + procs.add(proc); + } + dumpProcessSummaryLocked(pw, " ", procs, ALL_SCREEN_ADJ, ALL_MEM_ADJ, + NON_CACHED_PROC_STATES, false, now, totalTime); } - pw.print(" * "); pw.print(pkgName); pw.print(" / "); - UserHandle.formatUid(pw, uid); pw.println(":"); - } - if (!dumpSummary || dumpAll) { - for (int iproc=0; iproc<NPROCS; iproc++) { - ProcessState proc = pkgState.mProcesses.valueAt(iproc); - if (!pkgMatch && !reqPackage.equals(proc.mName)) { + for (int isvc=0; isvc<NSRVS; isvc++) { + ServiceState svc = pkgState.mServices.valueAt(isvc); + if (!pkgMatch && !reqPackage.equals(svc.mProcessName)) { continue; } - if (activeOnly && !proc.isInUse()) { + if (activeOnly && !svc.isInUse()) { pw.print(" (Not active: "); - pw.print(pkgState.mProcesses.keyAt(iproc)); pw.println(")"); + pw.print(pkgState.mServices.keyAt(isvc)); pw.println(")"); continue; } - pw.print(" Process "); - pw.print(pkgState.mProcesses.keyAt(iproc)); - if (proc.mCommonProcess.mMultiPackage) { - pw.print(" (multi, "); + if (dumpAll) { + pw.print(" Service "); } else { - pw.print(" (unique, "); + pw.print(" * "); } - pw.print(proc.mDurationsTableSize); - pw.print(" entries)"); + pw.print(pkgState.mServices.keyAt(isvc)); pw.println(":"); - dumpProcessState(pw, " ", proc, ALL_SCREEN_ADJ, ALL_MEM_ADJ, - ALL_PROC_STATES, now); - dumpProcessPss(pw, " ", proc, ALL_SCREEN_ADJ, ALL_MEM_ADJ, - ALL_PROC_STATES); - dumpProcessInternalLocked(pw, " ", proc, dumpAll); - } - } else { - ArrayList<ProcessState> procs = new ArrayList<ProcessState>(); - for (int iproc=0; iproc<NPROCS; iproc++) { - ProcessState proc = pkgState.mProcesses.valueAt(iproc); - if (!pkgMatch && !reqPackage.equals(proc.mName)) { - continue; - } - if (activeOnly && !proc.isInUse()) { - continue; - } - procs.add(proc); - } - dumpProcessSummaryLocked(pw, " ", procs, ALL_SCREEN_ADJ, ALL_MEM_ADJ, - NON_CACHED_PROC_STATES, now, totalTime); - } - for (int isvc=0; isvc<NSRVS; isvc++) { - ServiceState svc = pkgState.mServices.valueAt(isvc); - if (!pkgMatch && !reqPackage.equals(svc.mProcessName)) { - continue; - } - if (activeOnly && !svc.isInUse()) { - pw.print(" (Not active: "); - pw.print(pkgState.mServices.keyAt(isvc)); pw.println(")"); - continue; - } - if (dumpAll) { - pw.print(" Service "); - } else { - pw.print(" * "); - } - pw.print(pkgState.mServices.keyAt(isvc)); - pw.println(":"); - pw.print(" Process: "); pw.println(svc.mProcessName); - dumpServiceStats(pw, " ", " ", " ", "Running", svc, - svc.mRunCount, ServiceState.SERVICE_RUN, svc.mRunState, - svc.mRunStartTime, now, totalTime, !dumpSummary || dumpAll); - dumpServiceStats(pw, " ", " ", " ", "Started", svc, - svc.mStartedCount, ServiceState.SERVICE_STARTED, svc.mStartedState, - svc.mStartedStartTime, now, totalTime, !dumpSummary || dumpAll); - dumpServiceStats(pw, " ", " ", " ", "Bound", svc, - svc.mBoundCount, ServiceState.SERVICE_BOUND, svc.mBoundState, - svc.mBoundStartTime, now, totalTime, !dumpSummary || dumpAll); - dumpServiceStats(pw, " ", " ", " ", "Executing", svc, - svc.mExecCount, ServiceState.SERVICE_EXEC, svc.mExecState, - svc.mExecStartTime, now, totalTime, !dumpSummary || dumpAll); - if (dumpAll) { - if (svc.mOwner != null) { - pw.print(" mOwner="); pw.println(svc.mOwner); - } - if (svc.mStarted || svc.mRestarting) { - pw.print(" mStarted="); pw.print(svc.mStarted); - pw.print(" mRestarting="); pw.println(svc.mRestarting); + pw.print(" Process: "); pw.println(svc.mProcessName); + dumpServiceStats(pw, " ", " ", " ", "Running", svc, + svc.mRunCount, ServiceState.SERVICE_RUN, svc.mRunState, + svc.mRunStartTime, now, totalTime, !dumpSummary || dumpAll); + dumpServiceStats(pw, " ", " ", " ", "Started", svc, + svc.mStartedCount, ServiceState.SERVICE_STARTED, svc.mStartedState, + svc.mStartedStartTime, now, totalTime, !dumpSummary || dumpAll); + dumpServiceStats(pw, " ", " ", " ", "Bound", svc, + svc.mBoundCount, ServiceState.SERVICE_BOUND, svc.mBoundState, + svc.mBoundStartTime, now, totalTime, !dumpSummary || dumpAll); + dumpServiceStats(pw, " ", " ", " ", "Executing", svc, + svc.mExecCount, ServiceState.SERVICE_EXEC, svc.mExecState, + svc.mExecStartTime, now, totalTime, !dumpSummary || dumpAll); + if (dumpAll) { + if (svc.mOwner != null) { + pw.print(" mOwner="); pw.println(svc.mOwner); + } + if (svc.mStarted || svc.mRestarting) { + pw.print(" mStarted="); pw.print(svc.mStarted); + pw.print(" mRestarting="); pw.println(svc.mRestarting); + } } } } @@ -2059,7 +2141,7 @@ public final class ProcessStats implements Parcelable { pw.println(header); } dumpProcessSummaryLocked(pw, prefix, procs, screenStates, memStates, - sortProcStates, now, totalTime); + sortProcStates, true, now, totalTime); } } @@ -2067,23 +2149,27 @@ public final class ProcessStats implements Parcelable { int[] procStates, int sortProcStates[], long now, String reqPackage, boolean activeOnly) { final ArraySet<ProcessState> foundProcs = new ArraySet<ProcessState>(); - final ArrayMap<String, SparseArray<PackageState>> pkgMap = mPackages.getMap(); + final ArrayMap<String, SparseArray<SparseArray<PackageState>>> pkgMap = mPackages.getMap(); for (int ip=0; ip<pkgMap.size(); ip++) { final String pkgName = pkgMap.keyAt(ip); - final SparseArray<PackageState> procs = pkgMap.valueAt(ip); + final SparseArray<SparseArray<PackageState>> procs = pkgMap.valueAt(ip); for (int iu=0; iu<procs.size(); iu++) { - final PackageState state = procs.valueAt(iu); - final int NPROCS = state.mProcesses.size(); - final boolean pkgMatch = reqPackage == null || reqPackage.equals(pkgName); - for (int iproc=0; iproc<NPROCS; iproc++) { - final ProcessState proc = state.mProcesses.valueAt(iproc); - if (!pkgMatch && !reqPackage.equals(proc.mName)) { - continue; - } - if (activeOnly && !proc.isInUse()) { - continue; + final SparseArray<PackageState> vpkgs = procs.valueAt(iu); + final int NVERS = vpkgs.size(); + for (int iv=0; iv<NVERS; iv++) { + final PackageState state = vpkgs.valueAt(iv); + final int NPROCS = state.mProcesses.size(); + final boolean pkgMatch = reqPackage == null || reqPackage.equals(pkgName); + for (int iproc=0; iproc<NPROCS; iproc++) { + final ProcessState proc = state.mProcesses.valueAt(iproc); + if (!pkgMatch && !reqPackage.equals(proc.mName)) { + continue; + } + if (activeOnly && !proc.isInUse()) { + continue; + } + foundProcs.add(proc.mCommonProcess); } - foundProcs.add(proc.mCommonProcess); } } } @@ -2128,8 +2214,8 @@ public final class ProcessStats implements Parcelable { public void dumpCheckinLocked(PrintWriter pw, String reqPackage) { final long now = SystemClock.uptimeMillis(); - ArrayMap<String, SparseArray<PackageState>> pkgMap = mPackages.getMap(); - pw.println("vers,3"); + final ArrayMap<String, SparseArray<SparseArray<PackageState>>> pkgMap = mPackages.getMap(); + pw.println("vers,4"); pw.print("period,"); pw.print(mTimePeriodStartClockStr); pw.print(","); pw.print(mTimePeriodStartRealtime); pw.print(","); pw.print(mRunning ? SystemClock.elapsedRealtime() : mTimePeriodEndRealtime); @@ -2152,75 +2238,85 @@ public final class ProcessStats implements Parcelable { pw.println(); pw.print("config,"); pw.print(mRuntime); pw.print(','); pw.println(mWebView); for (int ip=0; ip<pkgMap.size(); ip++) { - String pkgName = pkgMap.keyAt(ip); + final String pkgName = pkgMap.keyAt(ip); if (reqPackage != null && !reqPackage.equals(pkgName)) { continue; } - SparseArray<PackageState> uids = pkgMap.valueAt(ip); + final SparseArray<SparseArray<PackageState>> uids = pkgMap.valueAt(ip); for (int iu=0; iu<uids.size(); iu++) { - int uid = uids.keyAt(iu); - PackageState pkgState = uids.valueAt(iu); - final int NPROCS = pkgState.mProcesses.size(); - final int NSRVS = pkgState.mServices.size(); - for (int iproc=0; iproc<NPROCS; iproc++) { - ProcessState proc = pkgState.mProcesses.valueAt(iproc); - pw.print("pkgproc,"); - pw.print(pkgName); - pw.print(","); - pw.print(uid); - pw.print(","); - pw.print(collapseString(pkgName, pkgState.mProcesses.keyAt(iproc))); - dumpAllProcessStateCheckin(pw, proc, now); - pw.println(); - if (proc.mPssTableSize > 0) { - pw.print("pkgpss,"); + final int uid = uids.keyAt(iu); + final SparseArray<PackageState> vpkgs = uids.valueAt(iu); + for (int iv=0; iv<vpkgs.size(); iv++) { + final int vers = vpkgs.keyAt(iv); + final PackageState pkgState = vpkgs.valueAt(iv); + final int NPROCS = pkgState.mProcesses.size(); + final int NSRVS = pkgState.mServices.size(); + for (int iproc=0; iproc<NPROCS; iproc++) { + ProcessState proc = pkgState.mProcesses.valueAt(iproc); + pw.print("pkgproc,"); pw.print(pkgName); pw.print(","); pw.print(uid); pw.print(","); - pw.print(collapseString(pkgName, pkgState.mProcesses.keyAt(iproc))); - dumpAllProcessPssCheckin(pw, proc); - pw.println(); - } - if (proc.mNumExcessiveWake > 0 || proc.mNumExcessiveCpu > 0 - || proc.mNumCachedKill > 0) { - pw.print("pkgkills,"); - pw.print(pkgName); - pw.print(","); - pw.print(uid); + pw.print(vers); pw.print(","); pw.print(collapseString(pkgName, pkgState.mProcesses.keyAt(iproc))); - pw.print(","); - pw.print(proc.mNumExcessiveWake); - pw.print(","); - pw.print(proc.mNumExcessiveCpu); - pw.print(","); - pw.print(proc.mNumCachedKill); - pw.print(","); - pw.print(proc.mMinCachedKillPss); - pw.print(":"); - pw.print(proc.mAvgCachedKillPss); - pw.print(":"); - pw.print(proc.mMaxCachedKillPss); + dumpAllProcessStateCheckin(pw, proc, now); pw.println(); + if (proc.mPssTableSize > 0) { + pw.print("pkgpss,"); + pw.print(pkgName); + pw.print(","); + pw.print(uid); + pw.print(","); + pw.print(vers); + pw.print(","); + pw.print(collapseString(pkgName, pkgState.mProcesses.keyAt(iproc))); + dumpAllProcessPssCheckin(pw, proc); + pw.println(); + } + if (proc.mNumExcessiveWake > 0 || proc.mNumExcessiveCpu > 0 + || proc.mNumCachedKill > 0) { + pw.print("pkgkills,"); + pw.print(pkgName); + pw.print(","); + pw.print(uid); + pw.print(","); + pw.print(vers); + pw.print(","); + pw.print(collapseString(pkgName, pkgState.mProcesses.keyAt(iproc))); + pw.print(","); + pw.print(proc.mNumExcessiveWake); + pw.print(","); + pw.print(proc.mNumExcessiveCpu); + pw.print(","); + pw.print(proc.mNumCachedKill); + pw.print(","); + pw.print(proc.mMinCachedKillPss); + pw.print(":"); + pw.print(proc.mAvgCachedKillPss); + pw.print(":"); + pw.print(proc.mMaxCachedKillPss); + pw.println(); + } + } + for (int isvc=0; isvc<NSRVS; isvc++) { + String serviceName = collapseString(pkgName, + pkgState.mServices.keyAt(isvc)); + ServiceState svc = pkgState.mServices.valueAt(isvc); + dumpServiceTimeCheckin(pw, "pkgsvc-run", pkgName, uid, vers, serviceName, + svc, ServiceState.SERVICE_RUN, svc.mRunCount, + svc.mRunState, svc.mRunStartTime, now); + dumpServiceTimeCheckin(pw, "pkgsvc-start", pkgName, uid, vers, serviceName, + svc, ServiceState.SERVICE_STARTED, svc.mStartedCount, + svc.mStartedState, svc.mStartedStartTime, now); + dumpServiceTimeCheckin(pw, "pkgsvc-bound", pkgName, uid, vers, serviceName, + svc, ServiceState.SERVICE_BOUND, svc.mBoundCount, + svc.mBoundState, svc.mBoundStartTime, now); + dumpServiceTimeCheckin(pw, "pkgsvc-exec", pkgName, uid, vers, serviceName, + svc, ServiceState.SERVICE_EXEC, svc.mExecCount, + svc.mExecState, svc.mExecStartTime, now); } - } - for (int isvc=0; isvc<NSRVS; isvc++) { - String serviceName = collapseString(pkgName, - pkgState.mServices.keyAt(isvc)); - ServiceState svc = pkgState.mServices.valueAt(isvc); - dumpServiceTimeCheckin(pw, "pkgsvc-run", pkgName, uid, serviceName, - svc, ServiceState.SERVICE_RUN, svc.mRunCount, - svc.mRunState, svc.mRunStartTime, now); - dumpServiceTimeCheckin(pw, "pkgsvc-start", pkgName, uid, serviceName, - svc, ServiceState.SERVICE_STARTED, svc.mStartedCount, - svc.mStartedState, svc.mStartedStartTime, now); - dumpServiceTimeCheckin(pw, "pkgsvc-bound", pkgName, uid, serviceName, - svc, ServiceState.SERVICE_BOUND, svc.mBoundCount, - svc.mBoundState, svc.mBoundStartTime, now); - dumpServiceTimeCheckin(pw, "pkgsvc-exec", pkgName, uid, serviceName, - svc, ServiceState.SERVICE_EXEC, svc.mExecCount, - svc.mExecState, svc.mExecStartTime, now); } } } @@ -2364,9 +2460,10 @@ public final class ProcessStats implements Parcelable { } public static final class ProcessState extends DurationsTable { - public final ProcessState mCommonProcess; + public ProcessState mCommonProcess; public final String mPackage; public final int mUid; + public final int mVersion; //final long[] mDurations = new long[STATE_COUNT*ADJ_COUNT]; int mCurState = STATE_NOTHING; @@ -2393,16 +2490,19 @@ public final class ProcessStats implements Parcelable { boolean mDead; public long mTmpTotalTime; + int mTmpNumInUse; + ProcessState mTmpFoundSubProc; /** * Create a new top-level process state, for the initial case where there is only * a single package running in a process. The initial state is not running. */ - public ProcessState(ProcessStats processStats, String pkg, int uid, String name) { + public ProcessState(ProcessStats processStats, String pkg, int uid, int vers, String name) { super(processStats, name); mCommonProcess = this; mPackage = pkg; mUid = uid; + mVersion = vers; } /** @@ -2410,18 +2510,19 @@ public final class ProcessStats implements Parcelable { * state. The current running state of the top-level process is also copied, * marked as started running at 'now'. */ - public ProcessState(ProcessState commonProcess, String pkg, int uid, String name, + public ProcessState(ProcessState commonProcess, String pkg, int uid, int vers, String name, long now) { super(commonProcess.mStats, name); mCommonProcess = commonProcess; mPackage = pkg; mUid = uid; + mVersion = vers; mCurState = commonProcess.mCurState; mStartTime = now; } ProcessState clone(String pkg, long now) { - ProcessState pnew = new ProcessState(this, pkg, mUid, mName, now); + ProcessState pnew = new ProcessState(this, pkg, mUid, mVersion, mName, now); copyDurationsTo(pnew); if (mPssTable != null) { mStats.mAddLongTable = new int[mPssTable.length]; @@ -2811,9 +2912,20 @@ public final class ProcessStats implements Parcelable { // The array map is still pointing to a common process state // that is now shared across packages. Update it to point to // the new per-package state. - ProcessState proc = mStats.mPackages.get(pkgName, mUid).mProcesses.get(mName); + SparseArray<PackageState> vpkg = mStats.mPackages.get(pkgName, mUid); + if (vpkg == null) { + throw new IllegalStateException("Didn't find package " + pkgName + + " / " + mUid); + } + PackageState pkg = vpkg.get(mVersion); + if (pkg == null) { + throw new IllegalStateException("Didn't find package " + pkgName + + " / " + mUid + " vers " + mVersion); + } + ProcessState proc = pkg.mProcesses.get(mName); if (proc == null) { - throw new IllegalStateException("Didn't create per-package process"); + throw new IllegalStateException("Didn't create per-package process " + + mName + " in pkg " + pkgName + " / " + mUid + " vers " + mVersion); } return proc; } @@ -2829,18 +2941,26 @@ public final class ProcessStats implements Parcelable { // are losing whatever data we had in the old process state. Log.wtf(TAG, "Pulling dead proc: name=" + mName + " pkg=" + mPackage + " uid=" + mUid + " common.name=" + mCommonProcess.mName); - proc = mStats.getProcessStateLocked(proc.mPackage, proc.mUid, proc.mName); + proc = mStats.getProcessStateLocked(proc.mPackage, proc.mUid, proc.mVersion, + proc.mName); } if (proc.mMultiPackage) { // The array map is still pointing to a common process state // that is now shared across packages. Update it to point to // the new per-package state. - PackageState pkg = mStats.mPackages.get(pkgList.keyAt(index), proc.mUid); - if (pkg == null) { + SparseArray<PackageState> vpkg = mStats.mPackages.get(pkgList.keyAt(index), + proc.mUid); + if (vpkg == null) { throw new IllegalStateException("No existing package " + pkgList.keyAt(index) + "/" + proc.mUid + " for multi-proc " + proc.mName); } + PackageState pkg = vpkg.get(proc.mVersion); + if (pkg == null) { + throw new IllegalStateException("No existing package " + + pkgList.keyAt(index) + "/" + proc.mUid + + " for multi-proc " + proc.mName + " version " + proc.mVersion); + } proc = pkg.mProcesses.get(proc.mName); if (proc == null) { throw new IllegalStateException("Didn't create per-package process " @@ -3014,7 +3134,7 @@ public final class ProcessStats implements Parcelable { } public boolean isInUse() { - return mOwner != null; + return mOwner != null || mRestarting; } void add(ServiceState other) { diff --git a/core/java/com/android/internal/app/ToolbarActionBar.java b/core/java/com/android/internal/app/ToolbarActionBar.java new file mode 100644 index 0000000..afb6f7c --- /dev/null +++ b/core/java/com/android/internal/app/ToolbarActionBar.java @@ -0,0 +1,447 @@ +/* + * 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.internal.app; + +import android.annotation.Nullable; +import android.app.ActionBar; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.drawable.Drawable; +import android.view.ActionMode; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.SpinnerAdapter; +import android.widget.Toolbar; + +import java.util.ArrayList; +import java.util.Map; + +public class ToolbarActionBar extends ActionBar { + private Toolbar mToolbar; + private View mCustomView; + + private int mDisplayOptions; + + private int mNavResId; + private int mIconResId; + private int mLogoResId; + private Drawable mNavDrawable; + private Drawable mIconDrawable; + private Drawable mLogoDrawable; + private int mTitleResId; + private int mSubtitleResId; + private CharSequence mTitle; + private CharSequence mSubtitle; + + private boolean mLastMenuVisibility; + private ArrayList<OnMenuVisibilityListener> mMenuVisibilityListeners = + new ArrayList<OnMenuVisibilityListener>(); + + public ToolbarActionBar(Toolbar toolbar) { + mToolbar = toolbar; + } + + @Override + public void setCustomView(View view) { + setCustomView(view, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); + } + + @Override + public void setCustomView(View view, LayoutParams layoutParams) { + if (mCustomView != null) { + mToolbar.removeView(mCustomView); + } + mCustomView = view; + if (view != null) { + mToolbar.addView(view, generateLayoutParams(layoutParams)); + } + } + + private Toolbar.LayoutParams generateLayoutParams(LayoutParams lp) { + final Toolbar.LayoutParams result = new Toolbar.LayoutParams(lp); + result.gravity = lp.gravity; + return result; + } + + @Override + public void setCustomView(int resId) { + final LayoutInflater inflater = LayoutInflater.from(mToolbar.getContext()); + setCustomView(inflater.inflate(resId, mToolbar, false)); + } + + @Override + public void setIcon(int resId) { + mIconResId = resId; + mIconDrawable = null; + updateToolbarLogo(); + } + + @Override + public void setIcon(Drawable icon) { + mIconResId = 0; + mIconDrawable = icon; + updateToolbarLogo(); + } + + @Override + public void setLogo(int resId) { + mLogoResId = resId; + mLogoDrawable = null; + updateToolbarLogo(); + } + + @Override + public void setLogo(Drawable logo) { + mLogoResId = 0; + mLogoDrawable = logo; + updateToolbarLogo(); + } + + private void updateToolbarLogo() { + Drawable drawable = null; + if ((mDisplayOptions & ActionBar.DISPLAY_SHOW_HOME) != 0) { + final int resId; + if ((mDisplayOptions & ActionBar.DISPLAY_USE_LOGO) != 0) { + resId = mLogoResId; + drawable = mLogoDrawable; + } else { + resId = mIconResId; + drawable = mIconDrawable; + } + if (resId != 0) { + drawable = mToolbar.getContext().getDrawable(resId); + } + } + mToolbar.setLogo(drawable); + } + + @Override + public void setStackedBackgroundDrawable(Drawable d) { + // This space for rent (do nothing) + } + + @Override + public void setSplitBackgroundDrawable(Drawable d) { + // This space for rent (do nothing) + } + + @Override + public void setHomeButtonEnabled(boolean enabled) { + // If the nav button on a Toolbar is present, it's enabled. No-op. + } + + @Override + public Context getThemedContext() { + return mToolbar.getContext(); + } + + @Override + public boolean isTitleTruncated() { + return super.isTitleTruncated(); + } + + @Override + public void setHomeAsUpIndicator(Drawable indicator) { + mToolbar.setNavigationIcon(indicator); + } + + @Override + public void setHomeAsUpIndicator(int resId) { + mToolbar.setNavigationIcon(resId); + } + + @Override + public void setHomeActionContentDescription(CharSequence description) { + mToolbar.setNavigationDescription(description); + } + + @Override + public void setDefaultDisplayHomeAsUpEnabled(boolean enabled) { + // Do nothing + } + + @Override + public void setHomeActionContentDescription(int resId) { + mToolbar.setNavigationDescription(resId); + } + + @Override + public void setShowHideAnimationEnabled(boolean enabled) { + // This space for rent; no-op. + } + + @Override + public void onConfigurationChanged(Configuration config) { + super.onConfigurationChanged(config); + } + + @Override + public ActionMode startActionMode(ActionMode.Callback callback) { + return mToolbar.startActionMode(callback); + } + + @Override + public void setListNavigationCallbacks(SpinnerAdapter adapter, OnNavigationListener callback) { + throw new UnsupportedOperationException( + "Navigation modes are not supported in toolbar action bars"); + } + + @Override + public void setSelectedNavigationItem(int position) { + throw new UnsupportedOperationException( + "Navigation modes are not supported in toolbar action bars"); + } + + @Override + public int getSelectedNavigationIndex() { + return -1; + } + + @Override + public int getNavigationItemCount() { + return 0; + } + + @Override + public void setTitle(CharSequence title) { + mTitle = title; + mTitleResId = 0; + updateToolbarTitle(); + } + + @Override + public void setTitle(int resId) { + mTitleResId = resId; + mTitle = null; + updateToolbarTitle(); + } + + @Override + public void setSubtitle(CharSequence subtitle) { + mSubtitle = subtitle; + mSubtitleResId = 0; + updateToolbarTitle(); + } + + @Override + public void setSubtitle(int resId) { + mSubtitleResId = resId; + mSubtitle = null; + updateToolbarTitle(); + } + + private void updateToolbarTitle() { + final Context context = mToolbar.getContext(); + CharSequence title = null; + CharSequence subtitle = null; + if ((mDisplayOptions & ActionBar.DISPLAY_SHOW_TITLE) != 0) { + title = mTitleResId != 0 ? context.getText(mTitleResId) : mTitle; + subtitle = mSubtitleResId != 0 ? context.getText(mSubtitleResId) : mSubtitle; + } + mToolbar.setTitle(title); + mToolbar.setSubtitle(subtitle); + } + + @Override + public void setDisplayOptions(@DisplayOptions int options) { + setDisplayOptions(options, 0xffffffff); + } + + @Override + public void setDisplayOptions(@DisplayOptions int options, @DisplayOptions int mask) { + final int oldOptions = mDisplayOptions; + mDisplayOptions = (options & mask) | (mDisplayOptions & ~mask); + final int optionsChanged = oldOptions ^ mDisplayOptions; + } + + @Override + public void setDisplayUseLogoEnabled(boolean useLogo) { + setDisplayOptions(useLogo ? DISPLAY_USE_LOGO : 0, DISPLAY_USE_LOGO); + } + + @Override + public void setDisplayShowHomeEnabled(boolean showHome) { + setDisplayOptions(showHome ? DISPLAY_SHOW_HOME : 0, DISPLAY_SHOW_HOME); + } + + @Override + public void setDisplayHomeAsUpEnabled(boolean showHomeAsUp) { + setDisplayOptions(showHomeAsUp ? DISPLAY_HOME_AS_UP : 0, DISPLAY_HOME_AS_UP); + } + + @Override + public void setDisplayShowTitleEnabled(boolean showTitle) { + setDisplayOptions(showTitle ? DISPLAY_SHOW_TITLE : 0, DISPLAY_SHOW_TITLE); + } + + @Override + public void setDisplayShowCustomEnabled(boolean showCustom) { + setDisplayOptions(showCustom ? DISPLAY_SHOW_CUSTOM : 0, DISPLAY_SHOW_CUSTOM); + } + + @Override + public void setBackgroundDrawable(@Nullable Drawable d) { + mToolbar.setBackground(d); + } + + @Override + public View getCustomView() { + return mCustomView; + } + + @Override + public CharSequence getTitle() { + return mToolbar.getTitle(); + } + + @Override + public CharSequence getSubtitle() { + return mToolbar.getSubtitle(); + } + + @Override + public int getNavigationMode() { + return NAVIGATION_MODE_STANDARD; + } + + @Override + public void setNavigationMode(@NavigationMode int mode) { + throw new UnsupportedOperationException( + "Navigation modes are not supported in toolbar action bars"); + } + + @Override + public int getDisplayOptions() { + return mDisplayOptions; + } + + @Override + public Tab newTab() { + throw new UnsupportedOperationException( + "Navigation modes are not supported in toolbar action bars"); + } + + @Override + public void addTab(Tab tab) { + throw new UnsupportedOperationException( + "Navigation modes are not supported in toolbar action bars"); + } + + @Override + public void addTab(Tab tab, boolean setSelected) { + throw new UnsupportedOperationException( + "Navigation modes are not supported in toolbar action bars"); + } + + @Override + public void addTab(Tab tab, int position) { + throw new UnsupportedOperationException( + "Navigation modes are not supported in toolbar action bars"); + } + + @Override + public void addTab(Tab tab, int position, boolean setSelected) { + throw new UnsupportedOperationException( + "Navigation modes are not supported in toolbar action bars"); + } + + @Override + public void removeTab(Tab tab) { + throw new UnsupportedOperationException( + "Navigation modes are not supported in toolbar action bars"); + } + + @Override + public void removeTabAt(int position) { + throw new UnsupportedOperationException( + "Navigation modes are not supported in toolbar action bars"); + } + + @Override + public void removeAllTabs() { + throw new UnsupportedOperationException( + "Navigation modes are not supported in toolbar action bars"); + } + + @Override + public void selectTab(Tab tab) { + throw new UnsupportedOperationException( + "Navigation modes are not supported in toolbar action bars"); + } + + @Override + public Tab getSelectedTab() { + throw new UnsupportedOperationException( + "Navigation modes are not supported in toolbar action bars"); + } + + @Override + public Tab getTabAt(int index) { + throw new UnsupportedOperationException( + "Navigation modes are not supported in toolbar action bars"); + } + + @Override + public int getTabCount() { + return 0; + } + + @Override + public int getHeight() { + return mToolbar.getHeight(); + } + + @Override + public void show() { + // TODO: Consider a better transition for this. + // Right now use no automatic transition so that the app can supply one if desired. + mToolbar.setVisibility(View.VISIBLE); + } + + @Override + public void hide() { + // TODO: Consider a better transition for this. + // Right now use no automatic transition so that the app can supply one if desired. + mToolbar.setVisibility(View.GONE); + } + + @Override + public boolean isShowing() { + return mToolbar.getVisibility() == View.VISIBLE; + } + + public void addOnMenuVisibilityListener(OnMenuVisibilityListener listener) { + mMenuVisibilityListeners.add(listener); + } + + public void removeOnMenuVisibilityListener(OnMenuVisibilityListener listener) { + mMenuVisibilityListeners.remove(listener); + } + + public void dispatchMenuVisibilityChanged(boolean isVisible) { + if (isVisible == mLastMenuVisibility) { + return; + } + mLastMenuVisibility = isVisible; + + final int count = mMenuVisibilityListeners.size(); + for (int i = 0; i < count; i++) { + mMenuVisibilityListeners.get(i).onMenuVisibilityChanged(isVisible); + } + } +} diff --git a/core/java/com/android/internal/app/ActionBarImpl.java b/core/java/com/android/internal/app/WindowDecorActionBar.java index ad45894..66548f0 100644 --- a/core/java/com/android/internal/app/ActionBarImpl.java +++ b/core/java/com/android/internal/app/WindowDecorActionBar.java @@ -17,7 +17,9 @@ package com.android.internal.app; import android.animation.ValueAnimator; +import android.content.res.TypedArray; import android.view.ViewParent; +import com.android.internal.R; import com.android.internal.view.ActionBarPolicy; import com.android.internal.view.menu.MenuBuilder; import com.android.internal.view.menu.MenuPopupHelper; @@ -41,8 +43,6 @@ import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.drawable.Drawable; -import android.os.Handler; -import android.util.Log; import android.util.TypedValue; import android.view.ActionMode; import android.view.ContextThemeWrapper; @@ -51,7 +51,6 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; -import android.view.ViewGroup; import android.view.Window; import android.view.accessibility.AccessibilityEvent; import android.view.animation.AnimationUtils; @@ -61,14 +60,15 @@ import java.lang.ref.WeakReference; import java.util.ArrayList; /** - * ActionBarImpl is the ActionBar implementation used - * by devices of all screen sizes. If it detects a compatible decor, - * it will split contextual modes across both the ActionBarView at - * the top of the screen and a horizontal LinearLayout at the bottom - * which is normally hidden. + * WindowDecorActionBar is the ActionBar implementation used + * by devices of all screen sizes as part of the window decor layout. + * If it detects a compatible decor, it will split contextual modes + * across both the ActionBarView at the top of the screen and + * a horizontal LinearLayout at the bottom which is normally hidden. */ -public class ActionBarImpl extends ActionBar { - private static final String TAG = "ActionBarImpl"; +public class WindowDecorActionBar extends ActionBar implements + ActionBarOverlayLayout.ActionBarVisibilityCallback { + private static final String TAG = "WindowDecorActionBar"; private Context mContext; private Context mThemedContext; @@ -106,9 +106,6 @@ public class ActionBarImpl extends ActionBar { private int mContextDisplayMode; private boolean mHasEmbeddedTabs; - final Handler mHandler = new Handler(); - Runnable mTabSelector; - private int mCurWindowVisibility = View.VISIBLE; private boolean mContentAnimations = true; @@ -120,6 +117,7 @@ public class ActionBarImpl extends ActionBar { private Animator mCurrentShowAnim; private boolean mShowHideAnimationEnabled; + boolean mHideOnContentScroll; final AnimatorListener mHideListener = new AnimatorListenerAdapter() { @Override @@ -136,7 +134,7 @@ public class ActionBarImpl extends ActionBar { mCurrentShowAnim = null; completeDeferredDestroyActionMode(); if (mOverlayLayout != null) { - mOverlayLayout.requestFitSystemWindows(); + mOverlayLayout.requestApplyInsets(); } } }; @@ -158,7 +156,7 @@ public class ActionBarImpl extends ActionBar { } }; - public ActionBarImpl(Activity activity) { + public WindowDecorActionBar(Activity activity) { mActivity = activity; Window window = activity.getWindow(); View decor = window.getDecorView(); @@ -169,7 +167,7 @@ public class ActionBarImpl extends ActionBar { } } - public ActionBarImpl(Dialog dialog) { + public WindowDecorActionBar(Dialog dialog) { mDialog = dialog; init(dialog.getWindow().getDecorView()); } @@ -178,17 +176,16 @@ public class ActionBarImpl extends ActionBar { * Only for edit mode. * @hide */ - public ActionBarImpl(View layout) { + public WindowDecorActionBar(View layout) { assert layout.isInEditMode(); init(layout); } private void init(View decor) { - mContext = decor.getContext(); mOverlayLayout = (ActionBarOverlayLayout) decor.findViewById( com.android.internal.R.id.action_bar_overlay_layout); if (mOverlayLayout != null) { - mOverlayLayout.setActionBar(this); + mOverlayLayout.setActionBarVisibilityCallback(this); } mActionView = (ActionBarView) decor.findViewById(com.android.internal.R.id.action_bar); mContextView = (ActionBarContextView) decor.findViewById( @@ -203,6 +200,7 @@ public class ActionBarImpl extends ActionBar { "with a compatible window decor layout"); } + mContext = mActionView.getContext(); mActionView.setContextView(mContextView); mContextDisplayMode = mActionView.isSplitActionBar() ? CONTEXT_DISPLAY_SPLIT : CONTEXT_DISPLAY_NORMAL; @@ -217,6 +215,14 @@ public class ActionBarImpl extends ActionBar { ActionBarPolicy abp = ActionBarPolicy.get(mContext); setHomeButtonEnabled(abp.enableHomeButtonByDefault() || homeAsUp); setHasEmbeddedTabs(abp.hasEmbeddedTabs()); + + final TypedArray a = mContext.obtainStyledAttributes(null, + com.android.internal.R.styleable.ActionBar, + com.android.internal.R.attr.actionBarStyle, 0); + if (a.getBoolean(R.styleable.ActionBar_hideOnContentScroll, false)) { + setHideOnContentScrollEnabled(true); + } + a.recycle(); } public void onConfigurationChanged(Configuration newConfig) { @@ -238,17 +244,14 @@ public class ActionBarImpl extends ActionBar { if (isInTabMode) { mTabScrollView.setVisibility(View.VISIBLE); if (mOverlayLayout != null) { - mOverlayLayout.requestFitSystemWindows(); + mOverlayLayout.requestApplyInsets(); } } else { mTabScrollView.setVisibility(View.GONE); } } mActionView.setCollapsable(!mHasEmbeddedTabs && isInTabMode); - } - - public boolean hasNonEmbeddedTabs() { - return !mHasEmbeddedTabs && getNavigationMode() == NAVIGATION_MODE_TABS; + mOverlayLayout.setHasNonEmbeddedTabs(!mHasEmbeddedTabs && isInTabMode); } private void ensureTabsExist() { @@ -283,7 +286,7 @@ public class ActionBarImpl extends ActionBar { } } - public void setWindowVisibility(int visibility) { + public void onWindowVisibilityChanged(int visibility) { mCurWindowVisibility = visibility; } @@ -457,6 +460,7 @@ public class ActionBarImpl extends ActionBar { mActionMode.finish(); } + mOverlayLayout.setHideOnContentScrollEnabled(false); mContextView.killMode(); ActionModeImpl mode = new ActionModeImpl(callback); if (mode.dispatchOnCreate()) { @@ -468,7 +472,7 @@ public class ActionBarImpl extends ActionBar { if (mSplitView.getVisibility() != View.VISIBLE) { mSplitView.setVisibility(View.VISIBLE); if (mOverlayLayout != null) { - mOverlayLayout.requestFitSystemWindows(); + mOverlayLayout.requestApplyInsets(); } } } @@ -656,6 +660,35 @@ public class ActionBarImpl extends ActionBar { } } + @Override + public void setHideOnContentScrollEnabled(boolean hideOnContentScroll) { + if (hideOnContentScroll && !mOverlayLayout.isInOverlayMode()) { + throw new IllegalStateException("Action bar must be in overlay mode " + + "(Window.FEATURE_OVERLAY_ACTION_BAR) to enable hide on content scroll"); + } + mHideOnContentScroll = hideOnContentScroll; + mOverlayLayout.setHideOnContentScrollEnabled(hideOnContentScroll); + } + + @Override + public boolean isHideOnContentScrollEnabled() { + return mOverlayLayout.isHideOnContentScrollEnabled(); + } + + @Override + public int getHideOffset() { + return mOverlayLayout.getActionBarHideOffset(); + } + + @Override + public void setHideOffset(int offset) { + if (offset != 0 && !mOverlayLayout.isInOverlayMode()) { + throw new IllegalStateException("Action bar must be in overlay mode " + + "(Window.FEATURE_OVERLAY_ACTION_BAR) to set a non-zero hide offset"); + } + mOverlayLayout.setActionBarHideOffset(offset); + } + private static boolean checkShowingFlags(boolean hiddenByApp, boolean hiddenBySystem, boolean showingForMode) { if (showingForMode) { @@ -741,7 +774,7 @@ public class ActionBarImpl extends ActionBar { mShowListener.onAnimationEnd(null); } if (mOverlayLayout != null) { - mOverlayLayout.requestFitSystemWindows(); + mOverlayLayout.requestApplyInsets(); } } @@ -785,11 +818,7 @@ public class ActionBarImpl extends ActionBar { } public boolean isShowing() { - return mNowShowing; - } - - public boolean isSystemShowing() { - return !mHiddenBySystem; + return mNowShowing && getHideOffset() < getHeight(); } void animateToMode(boolean toActionMode) { @@ -848,6 +877,18 @@ public class ActionBarImpl extends ActionBar { mActionView.setHomeActionContentDescription(resId); } + @Override + public void onContentScrollStarted() { + if (mCurrentShowAnim != null) { + mCurrentShowAnim.cancel(); + mCurrentShowAnim = null; + } + } + + @Override + public void onContentScrollStopped() { + } + /** * @hide */ @@ -898,6 +939,7 @@ public class ActionBarImpl extends ActionBar { // Clear out the context mode views after the animation finishes mContextView.closeMode(); mActionView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + mOverlayLayout.setHideOnContentScrollEnabled(mHideOnContentScroll); mActionMode = null; } @@ -1092,7 +1134,7 @@ public class ActionBarImpl extends ActionBar { @Override public Tab setIcon(int resId) { - return setIcon(mContext.getResources().getDrawable(resId)); + return setIcon(mContext.getDrawable(resId)); } @Override @@ -1208,6 +1250,7 @@ public class ActionBarImpl extends ActionBar { break; } mActionView.setCollapsable(mode == NAVIGATION_MODE_TABS && !mHasEmbeddedTabs); + mOverlayLayout.setHasNonEmbeddedTabs(mode == NAVIGATION_MODE_TABS && !mHasEmbeddedTabs); } @Override diff --git a/core/java/com/android/internal/appwidget/IAppWidgetService.aidl b/core/java/com/android/internal/appwidget/IAppWidgetService.aidl index 7ddd5d2..5214dd9 100644 --- a/core/java/com/android/internal/appwidget/IAppWidgetService.aidl +++ b/core/java/com/android/internal/appwidget/IAppWidgetService.aidl @@ -59,6 +59,5 @@ interface IAppWidgetService { void bindRemoteViewsService(int appWidgetId, in Intent intent, in IBinder connection, int userId); void unbindRemoteViewsService(int appWidgetId, in Intent intent, int userId); int[] getAppWidgetIds(in ComponentName provider, int userId); - } diff --git a/core/java/com/android/internal/backup/LocalTransport.java b/core/java/com/android/internal/backup/LocalTransport.java index 494bc78..446ef55 100644 --- a/core/java/com/android/internal/backup/LocalTransport.java +++ b/core/java/com/android/internal/backup/LocalTransport.java @@ -23,22 +23,24 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; import android.os.Environment; import android.os.ParcelFileDescriptor; -import android.os.RemoteException; import android.os.SELinux; +import android.system.ErrnoException; +import android.system.Os; +import android.system.StructStat; import android.util.Log; import com.android.org.bouncycastle.util.encoders.Base64; import java.io.File; -import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; + +import static android.system.OsConstants.*; /** * Backup transport for stashing stuff into a known location on disk, and @@ -55,20 +57,24 @@ public class LocalTransport extends IBackupTransport.Stub { private static final String TRANSPORT_DESTINATION_STRING = "Backing up to debug-only private cache"; - // The single hardcoded restore set always has the same (nonzero!) token - private static final long RESTORE_TOKEN = 1; + // The currently-active restore set always has the same (nonzero!) token + private static final long CURRENT_SET_TOKEN = 1; private Context mContext; private File mDataDir = new File(Environment.getDownloadCacheDirectory(), "backup"); + private File mCurrentSetDir = new File(mDataDir, Long.toString(CURRENT_SET_TOKEN)); + private PackageInfo[] mRestorePackages = null; private int mRestorePackage = -1; // Index into mRestorePackages + private File mRestoreDataDir; + private long mRestoreToken; public LocalTransport(Context context) { mContext = context; - mDataDir.mkdirs(); - if (!SELinux.restorecon(mDataDir)) { - Log.e(TAG, "SELinux restorecon failed for " + mDataDir); + mCurrentSetDir.mkdirs(); + if (!SELinux.restorecon(mCurrentSetDir)) { + Log.e(TAG, "SELinux restorecon failed for " + mCurrentSetDir); } } @@ -96,14 +102,23 @@ public class LocalTransport extends IBackupTransport.Stub { public int initializeDevice() { if (DEBUG) Log.v(TAG, "wiping all data"); - deleteContents(mDataDir); + deleteContents(mCurrentSetDir); return BackupConstants.TRANSPORT_OK; } public int performBackup(PackageInfo packageInfo, ParcelFileDescriptor data) { - if (DEBUG) Log.v(TAG, "performBackup() pkg=" + packageInfo.packageName); + if (DEBUG) { + try { + StructStat ss = Os.fstat(data.getFileDescriptor()); + Log.v(TAG, "performBackup() pkg=" + packageInfo.packageName + + " size=" + ss.st_size); + } catch (ErrnoException e) { + Log.w(TAG, "Unable to stat input file in performBackup() on " + + packageInfo.packageName); + } + } - File packageDir = new File(mDataDir, packageInfo.packageName); + File packageDir = new File(mCurrentSetDir, packageInfo.packageName); packageDir.mkdirs(); // Each 'record' in the restore set is kept in its own file, named by @@ -135,7 +150,16 @@ public class LocalTransport extends IBackupTransport.Stub { buf = new byte[bufSize]; } changeSet.readEntityData(buf, 0, dataSize); - if (DEBUG) Log.v(TAG, " data size " + dataSize); + if (DEBUG) { + try { + long cur = Os.lseek(data.getFileDescriptor(), 0, SEEK_CUR); + Log.v(TAG, " read entity data; new pos=" + cur); + } + catch (ErrnoException e) { + Log.w(TAG, "Unable to stat input file in performBackup() on " + + packageInfo.packageName); + } + } try { entity.write(buf, 0, dataSize); @@ -175,7 +199,7 @@ public class LocalTransport extends IBackupTransport.Stub { public int clearBackupData(PackageInfo packageInfo) { if (DEBUG) Log.v(TAG, "clearBackupData() pkg=" + packageInfo.packageName); - File packageDir = new File(mDataDir, packageInfo.packageName); + File packageDir = new File(mCurrentSetDir, packageInfo.packageName); final File[] fileset = packageDir.listFiles(); if (fileset != null) { for (File f : fileset) { @@ -192,22 +216,38 @@ public class LocalTransport extends IBackupTransport.Stub { } // Restore handling + static final long[] POSSIBLE_SETS = { 2, 3, 4, 5, 6, 7, 8, 9 }; public RestoreSet[] getAvailableRestoreSets() throws android.os.RemoteException { - // one hardcoded restore set - RestoreSet set = new RestoreSet("Local disk image", "flash", RESTORE_TOKEN); - RestoreSet[] array = { set }; - return array; + long[] existing = new long[POSSIBLE_SETS.length + 1]; + int num = 0; + + // see which possible non-current sets exist, then put the current set at the end + for (long token : POSSIBLE_SETS) { + if ((new File(mDataDir, Long.toString(token))).exists()) { + existing[num++] = token; + } + } + // and always the currently-active set last + existing[num++] = CURRENT_SET_TOKEN; + + RestoreSet[] available = new RestoreSet[num]; + for (int i = 0; i < available.length; i++) { + available[i] = new RestoreSet("Local disk image", "flash", existing[i]); + } + return available; } public long getCurrentRestoreSet() { - // The hardcoded restore set always has the same token - return RESTORE_TOKEN; + // The current restore set always has the same token + return CURRENT_SET_TOKEN; } public int startRestore(long token, PackageInfo[] packages) { if (DEBUG) Log.v(TAG, "start restore " + token); mRestorePackages = packages; mRestorePackage = -1; + mRestoreToken = token; + mRestoreDataDir = new File(mDataDir, Long.toString(token)); return BackupConstants.TRANSPORT_OK; } @@ -215,7 +255,9 @@ public class LocalTransport extends IBackupTransport.Stub { if (mRestorePackages == null) throw new IllegalStateException("startRestore not called"); while (++mRestorePackage < mRestorePackages.length) { String name = mRestorePackages[mRestorePackage].packageName; - if (new File(mDataDir, name).isDirectory()) { + // skip packages where we have a data dir but no actual contents + String[] contents = (new File(mRestoreDataDir, name)).list(); + if (contents != null && contents.length > 0) { if (DEBUG) Log.v(TAG, " nextRestorePackage() = " + name); return name; } @@ -228,29 +270,32 @@ public class LocalTransport extends IBackupTransport.Stub { public int getRestoreData(ParcelFileDescriptor outFd) { if (mRestorePackages == null) throw new IllegalStateException("startRestore not called"); if (mRestorePackage < 0) throw new IllegalStateException("nextRestorePackage not called"); - File packageDir = new File(mDataDir, mRestorePackages[mRestorePackage].packageName); + File packageDir = new File(mRestoreDataDir, mRestorePackages[mRestorePackage].packageName); // The restore set is the concatenation of the individual record blobs, - // each of which is a file in the package's directory - File[] blobs = packageDir.listFiles(); + // each of which is a file in the package's directory. We return the + // data in lexical order sorted by key, so that apps which use synthetic + // keys like BLOB_1, BLOB_2, etc will see the date in the most obvious + // order. + ArrayList<DecodedFilename> blobs = contentsByKey(packageDir); if (blobs == null) { // nextRestorePackage() ensures the dir exists, so this is an error - Log.e(TAG, "Error listing directory: " + packageDir); + Log.e(TAG, "No keys for package: " + packageDir); return BackupConstants.TRANSPORT_ERROR; } // We expect at least some data if the directory exists in the first place - if (DEBUG) Log.v(TAG, " getRestoreData() found " + blobs.length + " key files"); + if (DEBUG) Log.v(TAG, " getRestoreData() found " + blobs.size() + " key files"); BackupDataOutput out = new BackupDataOutput(outFd.getFileDescriptor()); try { - for (File f : blobs) { + for (DecodedFilename keyEntry : blobs) { + File f = keyEntry.file; FileInputStream in = new FileInputStream(f); try { int size = (int) f.length(); byte[] buf = new byte[size]; in.read(buf); - String key = new String(Base64.decode(f.getName())); - if (DEBUG) Log.v(TAG, " ... key=" + key + " size=" + size); - out.writeEntityHeader(key, size); + if (DEBUG) Log.v(TAG, " ... key=" + keyEntry.key + " size=" + size); + out.writeEntityHeader(keyEntry.key, size); out.writeEntityData(buf, size); } finally { in.close(); @@ -263,6 +308,39 @@ public class LocalTransport extends IBackupTransport.Stub { } } + static class DecodedFilename implements Comparable<DecodedFilename> { + public File file; + public String key; + + public DecodedFilename(File f) { + file = f; + key = new String(Base64.decode(f.getName())); + } + + @Override + public int compareTo(DecodedFilename other) { + // sorts into ascending lexical order by decoded key + return key.compareTo(other.key); + } + } + + // Return a list of the files in the given directory, sorted lexically by + // the Base64-decoded file name, not by the on-disk filename + private ArrayList<DecodedFilename> contentsByKey(File dir) { + File[] allFiles = dir.listFiles(); + if (allFiles == null || allFiles.length == 0) { + return null; + } + + // Decode the filenames into keys then sort lexically by key + ArrayList<DecodedFilename> contents = new ArrayList<DecodedFilename>(); + for (File f : allFiles) { + contents.add(new DecodedFilename(f)); + } + Collections.sort(contents); + return contents; + } + public void finishRestore() { if (DEBUG) Log.v(TAG, "finishRestore()"); } diff --git a/core/java/com/android/internal/content/PackageMonitor.java b/core/java/com/android/internal/content/PackageMonitor.java index 942995b..9df8ad5 100644 --- a/core/java/com/android/internal/content/PackageMonitor.java +++ b/core/java/com/android/internal/content/PackageMonitor.java @@ -22,7 +22,6 @@ import android.content.Intent; import android.content.IntentFilter; import android.net.Uri; import android.os.Handler; -import android.os.HandlerThread; import android.os.Looper; import android.os.UserHandle; import com.android.internal.os.BackgroundThread; @@ -243,7 +242,11 @@ public abstract class PackageMonitor extends android.content.BroadcastReceiver { public boolean anyPackagesDisappearing() { return mDisappearingPackages != null; } - + + public boolean isReplacing() { + return mChangeType == PACKAGE_UPDATING; + } + public boolean isPackageModified(String packageName) { if (mModifiedPackages != null) { for (int i=mModifiedPackages.length-1; i>=0; i--) { diff --git a/core/java/com/android/internal/inputmethod/InputMethodSubtypeSwitchingController.java b/core/java/com/android/internal/inputmethod/InputMethodSubtypeSwitchingController.java new file mode 100644 index 0000000..cba09d1 --- /dev/null +++ b/core/java/com/android/internal/inputmethod/InputMethodSubtypeSwitchingController.java @@ -0,0 +1,300 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.inputmethod; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.inputmethod.InputMethodUtils.InputMethodSettings; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.text.TextUtils; +import android.util.Slog; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodSubtype; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.TreeMap; + +/** + * InputMethodSubtypeSwitchingController controls the switching behavior of the subtypes. + */ +public class InputMethodSubtypeSwitchingController { + private static final String TAG = InputMethodSubtypeSwitchingController.class.getSimpleName(); + private static final boolean DEBUG = false; + // TODO: Turn on this flag and add CTS when the platform starts expecting that all IMEs return + // true for supportsSwitchingToNextInputMethod(). + private static final boolean REQUIRE_SWITCHING_SUPPORT = false; + private static final int MAX_HISTORY_SIZE = 4; + private static final int NOT_A_SUBTYPE_ID = InputMethodUtils.NOT_A_SUBTYPE_ID; + + private static class SubtypeParams { + public final InputMethodInfo mImi; + public final InputMethodSubtype mSubtype; + public final long mTime; + + public SubtypeParams(InputMethodInfo imi, InputMethodSubtype subtype) { + mImi = imi; + mSubtype = subtype; + mTime = System.currentTimeMillis(); + } + } + + public static class ImeSubtypeListItem implements Comparable<ImeSubtypeListItem> { + public final CharSequence mImeName; + public final CharSequence mSubtypeName; + public final InputMethodInfo mImi; + public final int mSubtypeId; + private final boolean mIsSystemLocale; + private final boolean mIsSystemLanguage; + + public ImeSubtypeListItem(CharSequence imeName, CharSequence subtypeName, + InputMethodInfo imi, int subtypeId, String subtypeLocale, String systemLocale) { + mImeName = imeName; + mSubtypeName = subtypeName; + mImi = imi; + mSubtypeId = subtypeId; + if (TextUtils.isEmpty(subtypeLocale)) { + mIsSystemLocale = false; + mIsSystemLanguage = false; + } else { + mIsSystemLocale = subtypeLocale.equals(systemLocale); + mIsSystemLanguage = mIsSystemLocale + || subtypeLocale.startsWith(systemLocale.substring(0, 2)); + } + } + + @Override + public int compareTo(ImeSubtypeListItem other) { + if (TextUtils.isEmpty(mImeName)) { + return 1; + } + if (TextUtils.isEmpty(other.mImeName)) { + return -1; + } + if (!TextUtils.equals(mImeName, other.mImeName)) { + return mImeName.toString().compareTo(other.mImeName.toString()); + } + if (TextUtils.equals(mSubtypeName, other.mSubtypeName)) { + return 0; + } + if (mIsSystemLocale) { + return -1; + } + if (other.mIsSystemLocale) { + return 1; + } + if (mIsSystemLanguage) { + return -1; + } + if (other.mIsSystemLanguage) { + return 1; + } + if (TextUtils.isEmpty(mSubtypeName)) { + return 1; + } + if (TextUtils.isEmpty(other.mSubtypeName)) { + return -1; + } + return mSubtypeName.toString().compareTo(other.mSubtypeName.toString()); + } + } + + private static class InputMethodAndSubtypeList { + private final Context mContext; + // Used to load label + private final PackageManager mPm; + private final String mSystemLocaleStr; + private final InputMethodSettings mSettings; + + public InputMethodAndSubtypeList(Context context, InputMethodSettings settings) { + mContext = context; + mSettings = settings; + mPm = context.getPackageManager(); + final Locale locale = context.getResources().getConfiguration().locale; + mSystemLocaleStr = locale != null ? locale.toString() : ""; + } + + private final TreeMap<InputMethodInfo, List<InputMethodSubtype>> mSortedImmis = + new TreeMap<InputMethodInfo, List<InputMethodSubtype>>( + new Comparator<InputMethodInfo>() { + @Override + public int compare(InputMethodInfo imi1, InputMethodInfo imi2) { + if (imi2 == null) + return 0; + if (imi1 == null) + return 1; + if (mPm == null) { + return imi1.getId().compareTo(imi2.getId()); + } + CharSequence imiId1 = imi1.loadLabel(mPm) + "/" + imi1.getId(); + CharSequence imiId2 = imi2.loadLabel(mPm) + "/" + imi2.getId(); + return imiId1.toString().compareTo(imiId2.toString()); + } + }); + + public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList() { + return getSortedInputMethodAndSubtypeList(true, false, false); + } + + public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList( + boolean showSubtypes, boolean inputShown, boolean isScreenLocked) { + final ArrayList<ImeSubtypeListItem> imList = + new ArrayList<ImeSubtypeListItem>(); + final HashMap<InputMethodInfo, List<InputMethodSubtype>> immis = + mSettings.getExplicitlyOrImplicitlyEnabledInputMethodsAndSubtypeListLocked( + mContext); + if (immis == null || immis.size() == 0) { + return Collections.emptyList(); + } + mSortedImmis.clear(); + mSortedImmis.putAll(immis); + for (InputMethodInfo imi : mSortedImmis.keySet()) { + if (imi == null) { + continue; + } + List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypeList = immis.get(imi); + HashSet<String> enabledSubtypeSet = new HashSet<String>(); + for (InputMethodSubtype subtype : explicitlyOrImplicitlyEnabledSubtypeList) { + enabledSubtypeSet.add(String.valueOf(subtype.hashCode())); + } + final CharSequence imeLabel = imi.loadLabel(mPm); + if (showSubtypes && enabledSubtypeSet.size() > 0) { + final int subtypeCount = imi.getSubtypeCount(); + if (DEBUG) { + Slog.v(TAG, "Add subtypes: " + subtypeCount + ", " + imi.getId()); + } + for (int j = 0; j < subtypeCount; ++j) { + final InputMethodSubtype subtype = imi.getSubtypeAt(j); + final String subtypeHashCode = String.valueOf(subtype.hashCode()); + // We show all enabled IMEs and subtypes when an IME is shown. + if (enabledSubtypeSet.contains(subtypeHashCode) + && ((inputShown && !isScreenLocked) || !subtype.isAuxiliary())) { + final CharSequence subtypeLabel = + subtype.overridesImplicitlyEnabledSubtype() ? null : subtype + .getDisplayName(mContext, imi.getPackageName(), + imi.getServiceInfo().applicationInfo); + imList.add(new ImeSubtypeListItem(imeLabel, + subtypeLabel, imi, j, subtype.getLocale(), mSystemLocaleStr)); + + // Removing this subtype from enabledSubtypeSet because we no + // longer need to add an entry of this subtype to imList to avoid + // duplicated entries. + enabledSubtypeSet.remove(subtypeHashCode); + } + } + } else { + imList.add(new ImeSubtypeListItem(imeLabel, null, imi, NOT_A_SUBTYPE_ID, null, + mSystemLocaleStr)); + } + } + Collections.sort(imList); + return imList; + } + } + + private final ArrayDeque<SubtypeParams> mTypedSubtypeHistory = new ArrayDeque<SubtypeParams>(); + private final Object mLock = new Object(); + private final InputMethodSettings mSettings; + private InputMethodAndSubtypeList mSubtypeList; + + @VisibleForTesting + public static ImeSubtypeListItem getNextInputMethodImpl(List<ImeSubtypeListItem> imList, + boolean onlyCurrentIme, InputMethodInfo imi, InputMethodSubtype subtype) { + if (imi == null) { + return null; + } + if (imList.size() <= 1) { + return null; + } + final int N = imList.size(); + final int currentSubtypeId = + subtype != null ? InputMethodUtils.getSubtypeIdFromHashCode(imi, + subtype.hashCode()) : NOT_A_SUBTYPE_ID; + for (int i = 0; i < N; ++i) { + final ImeSubtypeListItem isli = imList.get(i); + if (isli.mImi.equals(imi) && isli.mSubtypeId == currentSubtypeId) { + if (!onlyCurrentIme) { + return imList.get((i + 1) % N); + } + for (int j = 0; j < N - 1; ++j) { + final ImeSubtypeListItem candidate = imList.get((i + j + 1) % N); + if (candidate.mImi.equals(imi)) { + return candidate; + } + } + return null; + } + } + return null; + } + + public InputMethodSubtypeSwitchingController(InputMethodSettings settings) { + mSettings = settings; + } + + // TODO: write unit tests for this method and the logic that determines the next subtype + public void onCommitText(InputMethodInfo imi, InputMethodSubtype subtype) { + synchronized (mTypedSubtypeHistory) { + if (subtype == null) { + Slog.w(TAG, "Invalid InputMethodSubtype: " + imi.getId() + ", " + subtype); + return; + } + if (DEBUG) { + Slog.d(TAG, "onCommitText: " + imi.getId() + ", " + subtype); + } + if (REQUIRE_SWITCHING_SUPPORT) { + if (!imi.supportsSwitchingToNextInputMethod()) { + Slog.w(TAG, imi.getId() + " doesn't support switching to next input method."); + return; + } + } + if (mTypedSubtypeHistory.size() >= MAX_HISTORY_SIZE) { + mTypedSubtypeHistory.poll(); + } + mTypedSubtypeHistory.addFirst(new SubtypeParams(imi, subtype)); + } + } + + public void resetCircularListLocked(Context context) { + synchronized(mLock) { + mSubtypeList = new InputMethodAndSubtypeList(context, mSettings); + } + } + + public ImeSubtypeListItem getNextInputMethod( + boolean onlyCurrentIme, InputMethodInfo imi, InputMethodSubtype subtype) { + synchronized(mLock) { + return getNextInputMethodImpl(mSubtypeList.getSortedInputMethodAndSubtypeList(), + onlyCurrentIme, imi, subtype); + } + } + + public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList(boolean showSubtypes, + boolean inputShown, boolean isScreenLocked) { + synchronized(mLock) { + return mSubtypeList.getSortedInputMethodAndSubtypeList( + showSubtypes, inputShown, isScreenLocked); + } + } +} diff --git a/core/java/com/android/internal/inputmethod/InputMethodUtils.java b/core/java/com/android/internal/inputmethod/InputMethodUtils.java index 63d018f..ac3274d 100644 --- a/core/java/com/android/internal/inputmethod/InputMethodUtils.java +++ b/core/java/com/android/internal/inputmethod/InputMethodUtils.java @@ -504,6 +504,7 @@ public class InputMethodUtils { private String mEnabledInputMethodsStrCache; private int mCurrentUserId; + private int[] mCurrentProfileIds = new int[0]; private static void buildEnabledInputMethodsSettingString( StringBuilder builder, Pair<String, ArrayList<String>> pair) { @@ -536,6 +537,22 @@ public class InputMethodUtils { mCurrentUserId = userId; } + public void setCurrentProfileIds(int[] currentProfileIds) { + synchronized (this) { + mCurrentProfileIds = currentProfileIds; + } + } + + public boolean isCurrentProfile(int userId) { + synchronized (this) { + if (userId == mCurrentUserId) return true; + for (int i = 0; i < mCurrentProfileIds.length; i++) { + if (userId == mCurrentProfileIds[i]) return true; + } + return false; + } + } + public List<InputMethodInfo> getEnabledInputMethodListLocked() { return createEnabledInputMethodListLocked( getEnabledInputMethodsAndSubtypeListLocked()); @@ -959,5 +976,16 @@ public class InputMethodUtils { addSubtypeToHistory(curMethodId, subtypeId); } } + + public HashMap<InputMethodInfo, List<InputMethodSubtype>> + getExplicitlyOrImplicitlyEnabledInputMethodsAndSubtypeListLocked(Context context) { + HashMap<InputMethodInfo, List<InputMethodSubtype>> enabledInputMethodAndSubtypes = + new HashMap<InputMethodInfo, List<InputMethodSubtype>>(); + for (InputMethodInfo imi: getEnabledInputMethodListLocked()) { + enabledInputMethodAndSubtypes.put( + imi, getEnabledInputMethodSubtypeListLocked(context, imi, true)); + } + return enabledInputMethodAndSubtypes; + } } } diff --git a/core/java/com/android/internal/net/NetworkStatsFactory.java b/core/java/com/android/internal/net/NetworkStatsFactory.java index 8282d23..e2a2b1e 100644 --- a/core/java/com/android/internal/net/NetworkStatsFactory.java +++ b/core/java/com/android/internal/net/NetworkStatsFactory.java @@ -17,6 +17,7 @@ package com.android.internal.net; import static android.net.NetworkStats.SET_ALL; +import static android.net.NetworkStats.TAG_ALL; import static android.net.NetworkStats.TAG_NONE; import static android.net.NetworkStats.UID_ALL; import static com.android.server.NetworkManagementSocketTagger.kernelToTag; @@ -26,6 +27,7 @@ import android.os.StrictMode; import android.os.SystemClock; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.ArrayUtils; import com.android.internal.util.ProcFileReader; import java.io.File; @@ -165,22 +167,32 @@ public class NetworkStatsFactory { } public NetworkStats readNetworkStatsDetail() throws IOException { - return readNetworkStatsDetail(UID_ALL); + return readNetworkStatsDetail(UID_ALL, null, TAG_ALL, null); } - public NetworkStats readNetworkStatsDetail(int limitUid) throws IOException { + public NetworkStats readNetworkStatsDetail(int limitUid, String[] limitIfaces, int limitTag, + NetworkStats lastStats) + throws IOException { if (USE_NATIVE_PARSING) { - final NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 0); - if (nativeReadNetworkStatsDetail(stats, mStatsXtUid.getAbsolutePath(), limitUid) != 0) { + final NetworkStats stats; + if (lastStats != null) { + stats = lastStats; + stats.setElapsedRealtime(SystemClock.elapsedRealtime()); + } else { + stats = new NetworkStats(SystemClock.elapsedRealtime(), -1); + } + if (nativeReadNetworkStatsDetail(stats, mStatsXtUid.getAbsolutePath(), limitUid, + limitIfaces, limitTag) != 0) { throw new IOException("Failed to parse network stats"); } if (SANITY_CHECK_NATIVE) { - final NetworkStats javaStats = javaReadNetworkStatsDetail(mStatsXtUid, limitUid); + final NetworkStats javaStats = javaReadNetworkStatsDetail(mStatsXtUid, limitUid, + limitIfaces, limitTag); assertEquals(javaStats, stats); } return stats; } else { - return javaReadNetworkStatsDetail(mStatsXtUid, limitUid); + return javaReadNetworkStatsDetail(mStatsXtUid, limitUid, limitIfaces, limitTag); } } @@ -189,7 +201,8 @@ public class NetworkStatsFactory { * expected to monotonically increase since device boot. */ @VisibleForTesting - public static NetworkStats javaReadNetworkStatsDetail(File detailPath, int limitUid) + public static NetworkStats javaReadNetworkStatsDetail(File detailPath, int limitUid, + String[] limitIfaces, int limitTag) throws IOException { final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads(); @@ -222,7 +235,9 @@ public class NetworkStatsFactory { entry.txBytes = reader.nextLong(); entry.txPackets = reader.nextLong(); - if (limitUid == UID_ALL || limitUid == entry.uid) { + if ((limitIfaces == null || ArrayUtils.contains(limitIfaces, entry.iface)) + && (limitUid == UID_ALL || limitUid == entry.uid) + && (limitTag == TAG_ALL || limitTag == entry.tag)) { stats.addValues(entry); } @@ -264,5 +279,5 @@ public class NetworkStatsFactory { */ @VisibleForTesting public static native int nativeReadNetworkStatsDetail( - NetworkStats stats, String path, int limitUid); + NetworkStats stats, String path, int limitUid, String[] limitIfaces, int limitTag); } diff --git a/core/java/com/android/internal/net/VpnConfig.java b/core/java/com/android/internal/net/VpnConfig.java index 98599d0..0d00f41 100644 --- a/core/java/com/android/internal/net/VpnConfig.java +++ b/core/java/com/android/internal/net/VpnConfig.java @@ -25,8 +25,6 @@ import android.os.UserHandle; import android.net.RouteInfo; import android.net.LinkAddress; -import com.android.internal.util.Preconditions; - import java.net.InetAddress; import java.util.List; import java.util.ArrayList; diff --git a/core/java/com/android/internal/notification/PeopleNotificationScorer.java b/core/java/com/android/internal/notification/PeopleNotificationScorer.java new file mode 100644 index 0000000..efb5f63 --- /dev/null +++ b/core/java/com/android/internal/notification/PeopleNotificationScorer.java @@ -0,0 +1,227 @@ +/* +* 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.internal.notification; + +import android.app.Notification; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.provider.ContactsContract; +import android.provider.ContactsContract.Contacts; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.LruCache; +import android.util.Slog; + +/** + * This {@link NotificationScorer} attempts to validate people references. + * Also elevates the priority of real people. + */ +public class PeopleNotificationScorer implements NotificationScorer { + private static final String TAG = "PeopleNotificationScorer"; + private static final boolean DBG = false; + + private static final boolean ENABLE_PEOPLE_SCORER = true; + private static final String SETTING_ENABLE_PEOPLE_SCORER = "people_scorer_enabled"; + private static final String[] LOOKUP_PROJECTION = { Contacts._ID }; + private static final int MAX_PEOPLE = 10; + private static final int PEOPLE_CACHE_SIZE = 200; + // see NotificationManagerService + private static final int NOTIFICATION_PRIORITY_MULTIPLIER = 10; + + protected boolean mEnabled; + private Context mContext; + + // maps raw person handle to resolved person object + private LruCache<String, LookupResult> mPeopleCache; + + private float findMaxContactScore(Bundle extras) { + if (extras == null) { + return 0f; + } + + final String[] people = extras.getStringArray(Notification.EXTRA_PEOPLE); + if (people == null || people.length == 0) { + return 0f; + } + + float rank = 0f; + for (int personIdx = 0; personIdx < people.length && personIdx < MAX_PEOPLE; personIdx++) { + final String handle = people[personIdx]; + if (TextUtils.isEmpty(handle)) continue; + + LookupResult lookupResult = mPeopleCache.get(handle); + if (lookupResult == null || lookupResult.isExpired()) { + final Uri uri = Uri.parse(handle); + if ("tel".equals(uri.getScheme())) { + if (DBG) Slog.w(TAG, "checking telephone URI: " + handle); + lookupResult = lookupPhoneContact(handle, uri.getSchemeSpecificPart()); + } else if (handle.startsWith(Contacts.CONTENT_LOOKUP_URI.toString())) { + if (DBG) Slog.w(TAG, "checking lookup URI: " + handle); + lookupResult = resolveContactsUri(handle, uri); + } else { + if (DBG) Slog.w(TAG, "unsupported URI " + handle); + } + } else { + if (DBG) Slog.w(TAG, "using cached lookupResult: " + lookupResult.mId); + } + if (lookupResult != null) { + rank = Math.max(rank, lookupResult.getRank()); + } + } + return rank; + } + + private LookupResult lookupPhoneContact(final String handle, final String number) { + LookupResult lookupResult = null; + Cursor c = null; + try { + Uri numberUri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, + Uri.encode(number)); + c = mContext.getContentResolver().query(numberUri, LOOKUP_PROJECTION, null, null, null); + if (c != null && c.getCount() > 0) { + c.moveToFirst(); + final int idIdx = c.getColumnIndex(Contacts._ID); + final int id = c.getInt(idIdx); + if (DBG) Slog.w(TAG, "is valid: " + id); + lookupResult = new LookupResult(id); + } + } catch(Throwable t) { + Slog.w(TAG, "Problem getting content resolver or performing contacts query.", t); + } finally { + if (c != null) { + c.close(); + } + } + if (lookupResult == null) { + lookupResult = new LookupResult(LookupResult.INVALID_ID); + } + mPeopleCache.put(handle, lookupResult); + return lookupResult; + } + + private LookupResult resolveContactsUri(String handle, final Uri personUri) { + LookupResult lookupResult = null; + Cursor c = null; + try { + c = mContext.getContentResolver().query(personUri, LOOKUP_PROJECTION, null, null, null); + if (c != null && c.getCount() > 0) { + c.moveToFirst(); + final int idIdx = c.getColumnIndex(Contacts._ID); + final int id = c.getInt(idIdx); + if (DBG) Slog.w(TAG, "is valid: " + id); + lookupResult = new LookupResult(id); + } + } catch(Throwable t) { + Slog.w(TAG, "Problem getting content resolver or performing contacts query.", t); + } finally { + if (c != null) { + c.close(); + } + } + if (lookupResult == null) { + lookupResult = new LookupResult(LookupResult.INVALID_ID); + } + mPeopleCache.put(handle, lookupResult); + return lookupResult; + } + + private final static int clamp(int x, int low, int high) { + return (x < low) ? low : ((x > high) ? high : x); + } + + // TODO: rework this function before shipping + private static int priorityBumpMap(int incomingScore) { + //assumption is that scale runs from [-2*pm, 2*pm] + int pm = NOTIFICATION_PRIORITY_MULTIPLIER; + int theScore = incomingScore; + // enforce input in range + theScore = clamp(theScore, -2 * pm, 2 * pm); + if (theScore != incomingScore) return incomingScore; + // map -20 -> -20 and -10 -> 5 (when pm = 10) + if (theScore <= -pm) { + theScore += 1.5 * (theScore + 2 * pm); + } else { + // map 0 -> 10, 10 -> 15, 20 -> 20; + theScore += 0.5 * (2 * pm - theScore); + } + if (DBG) Slog.v(TAG, "priorityBumpMap: score before: " + incomingScore + + ", score after " + theScore + "."); + return theScore; + } + + @Override + public void initialize(Context context) { + if (DBG) Slog.v(TAG, "Initializing " + getClass().getSimpleName() + "."); + mContext = context; + mPeopleCache = new LruCache<String, LookupResult>(PEOPLE_CACHE_SIZE); + mEnabled = ENABLE_PEOPLE_SCORER && 1 == Settings.Global.getInt( + mContext.getContentResolver(), SETTING_ENABLE_PEOPLE_SCORER, 0); + } + + @Override + public int getScore(Notification notification, int score) { + if (notification == null || !mEnabled) { + if (DBG) Slog.w(TAG, "empty notification? scorer disabled?"); + return score; + } + float contactScore = findMaxContactScore(notification.extras); + if (contactScore > 0f) { + if (DBG) Slog.v(TAG, "Notification references a real contact. Promoted!"); + score = priorityBumpMap(score); + } else { + if (DBG) Slog.v(TAG, "Notification lacks any valid contact reference. Not promoted!"); + } + return score; + } + + private static class LookupResult { + private static final long CONTACT_REFRESH_MILLIS = 60 * 60 * 1000; // 1hr + public static final int INVALID_ID = -1; + + private final long mExpireMillis; + private int mId; + + public LookupResult(int id) { + mId = id; + mExpireMillis = System.currentTimeMillis() + CONTACT_REFRESH_MILLIS; + } + + public boolean isExpired() { + return mExpireMillis < System.currentTimeMillis(); + } + + public boolean isInvalid() { + return mId == INVALID_ID || isExpired(); + } + + public float getRank() { + if (isInvalid()) { + return 0f; + } else { + return 1f; // TODO: finer grained score + } + } + + public LookupResult setId(int id) { + mId = id; + return this; + } + } +} + diff --git a/core/java/com/android/internal/os/BatterySipper.java b/core/java/com/android/internal/os/BatterySipper.java new file mode 100644 index 0000000..6ca24d7 --- /dev/null +++ b/core/java/com/android/internal/os/BatterySipper.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2009 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.internal.os; + +import android.os.BatteryStats.Uid; + +/** + * Contains power usage of an application, system service, or hardware type. + */ +public class BatterySipper implements Comparable<BatterySipper> { + public int userId; + public Uid uidObj; + public double value; + public double[] values; + public DrainType drainType; + public long usageTime; + public long cpuTime; + public long gpsTime; + public long wifiRunningTime; + public long cpuFgTime; + public long wakeLockTime; + public long mobileRxPackets; + public long mobileTxPackets; + public long mobileActive; + public int mobileActiveCount; + public double mobilemspp; // milliseconds per packet + public long wifiRxPackets; + public long wifiTxPackets; + public long mobileRxBytes; + public long mobileTxBytes; + public long wifiRxBytes; + public long wifiTxBytes; + public double percent; + public double noCoveragePercent; + public String[] mPackages; + public String packageWithHighestDrain; + + public enum DrainType { + IDLE, + CELL, + PHONE, + WIFI, + BLUETOOTH, + SCREEN, + APP, + USER, + UNACCOUNTED, + OVERCOUNTED + } + + public BatterySipper(DrainType drainType, Uid uid, double[] values) { + this.values = values; + if (values != null) value = values[0]; + this.drainType = drainType; + uidObj = uid; + } + + public double[] getValues() { + return values; + } + + public void computeMobilemspp() { + long packets = mobileRxPackets+mobileTxPackets; + mobilemspp = packets > 0 ? (mobileActive / (double)packets) : 0; + } + + @Override + public int compareTo(BatterySipper other) { + // Return the flipped value because we want the items in descending order + return Double.compare(other.value, value); + } + + /** + * Gets a list of packages associated with the current user + */ + public String[] getPackages() { + return mPackages; + } + + public int getUid() { + // Bail out if the current sipper is not an App sipper. + if (uidObj == null) { + return 0; + } + return uidObj.getUid(); + } +} diff --git a/core/java/com/android/internal/os/BatteryStatsHelper.java b/core/java/com/android/internal/os/BatteryStatsHelper.java new file mode 100644 index 0000000..7ff949e --- /dev/null +++ b/core/java/com/android/internal/os/BatteryStatsHelper.java @@ -0,0 +1,838 @@ +/* + * Copyright (C) 2009 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.internal.os; + +import static android.os.BatteryStats.NETWORK_MOBILE_RX_DATA; +import static android.os.BatteryStats.NETWORK_MOBILE_TX_DATA; +import static android.os.BatteryStats.NETWORK_WIFI_RX_DATA; +import static android.os.BatteryStats.NETWORK_WIFI_TX_DATA; + +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.hardware.Sensor; +import android.hardware.SensorManager; +import android.net.ConnectivityManager; +import android.os.BatteryStats; +import android.os.BatteryStats.Uid; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Process; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.os.UserHandle; +import android.telephony.SignalStrength; +import android.util.Log; +import android.util.SparseArray; + +import com.android.internal.app.IBatteryStats; +import com.android.internal.os.BatterySipper.DrainType; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +/** + * A helper class for retrieving the power usage information for all applications and services. + * + * The caller must initialize this class as soon as activity object is ready to use (for example, in + * onAttach() for Fragment), call create() in onCreate() and call destroy() in onDestroy(). + */ +public class BatteryStatsHelper { + + private static final boolean DEBUG = false; + + private static final String TAG = BatteryStatsHelper.class.getSimpleName(); + + private static BatteryStats sStatsXfer; + private static Intent sBatteryBroadcastXfer; + + final private Context mContext; + final private boolean mCollectBatteryBroadcast; + + private IBatteryStats mBatteryInfo; + private BatteryStats mStats; + private Intent mBatteryBroadcast; + private PowerProfile mPowerProfile; + + private final List<BatterySipper> mUsageList = new ArrayList<BatterySipper>(); + private final List<BatterySipper> mWifiSippers = new ArrayList<BatterySipper>(); + private final List<BatterySipper> mBluetoothSippers = new ArrayList<BatterySipper>(); + private final SparseArray<List<BatterySipper>> mUserSippers + = new SparseArray<List<BatterySipper>>(); + private final SparseArray<Double> mUserPower = new SparseArray<Double>(); + + private final List<BatterySipper> mMobilemsppList = new ArrayList<BatterySipper>(); + + private int mStatsType = BatteryStats.STATS_SINCE_CHARGED; + private int mAsUser = 0; + + long mRawRealtime; + long mRawUptime; + long mBatteryRealtime; + long mBatteryUptime; + long mTypeBatteryRealtime; + long mTypeBatteryUptime; + long mBatteryTimeRemaining; + long mChargeTimeRemaining; + + private long mStatsPeriod = 0; + private double mMaxPower = 1; + private double mComputedPower; + private double mTotalPower; + private double mWifiPower; + private double mBluetoothPower; + private double mMinDrainedPower; + private double mMaxDrainedPower; + + // How much the apps together have kept the mobile radio active. + private long mAppMobileActive; + + // How much the apps together have left WIFI running. + private long mAppWifiRunning; + + public BatteryStatsHelper(Context context) { + this(context, true); + } + + public BatteryStatsHelper(Context context, boolean collectBatteryBroadcast) { + mContext = context; + mCollectBatteryBroadcast = collectBatteryBroadcast; + } + + /** Clears the current stats and forces recreating for future use. */ + public void clearStats() { + mStats = null; + } + + public BatteryStats getStats() { + if (mStats == null) { + load(); + } + return mStats; + } + + public Intent getBatteryBroadcast() { + if (mBatteryBroadcast == null && mCollectBatteryBroadcast) { + load(); + } + return mBatteryBroadcast; + } + + public PowerProfile getPowerProfile() { + return mPowerProfile; + } + + public void create(BatteryStats stats) { + mPowerProfile = new PowerProfile(mContext); + mStats = stats; + } + + public void create(Bundle icicle) { + if (icicle != null) { + mStats = sStatsXfer; + mBatteryBroadcast = sBatteryBroadcastXfer; + } + mBatteryInfo = IBatteryStats.Stub.asInterface( + ServiceManager.getService(BatteryStats.SERVICE_NAME)); + mPowerProfile = new PowerProfile(mContext); + } + + public void storeState() { + sStatsXfer = mStats; + sBatteryBroadcastXfer = mBatteryBroadcast; + } + + public static String makemAh(double power) { + if (power < .00001) return String.format("%.8f", power); + else if (power < .0001) return String.format("%.7f", power); + else if (power < .001) return String.format("%.6f", power); + else if (power < .01) return String.format("%.5f", power); + else if (power < .1) return String.format("%.4f", power); + else if (power < 1) return String.format("%.3f", power); + else if (power < 10) return String.format("%.2f", power); + else if (power < 100) return String.format("%.1f", power); + else return String.format("%.0f", power); + } + + /** + * Refreshes the power usage list. + */ + public void refreshStats(int statsType, int asUser) { + refreshStats(statsType, asUser, SystemClock.elapsedRealtime() * 1000, + SystemClock.uptimeMillis() * 1000); + } + + public void refreshStats(int statsType, int asUser, long rawRealtimeUs, long rawUptimeUs) { + // Initialize mStats if necessary. + getStats(); + + mMaxPower = 0; + mComputedPower = 0; + mTotalPower = 0; + mWifiPower = 0; + mBluetoothPower = 0; + mAppMobileActive = 0; + mAppWifiRunning = 0; + + mUsageList.clear(); + mWifiSippers.clear(); + mBluetoothSippers.clear(); + mUserSippers.clear(); + mUserPower.clear(); + mMobilemsppList.clear(); + + if (mStats == null) { + return; + } + + mStatsType = statsType; + mAsUser = asUser; + mRawUptime = rawUptimeUs; + mRawRealtime = rawRealtimeUs; + mBatteryUptime = mStats.getBatteryUptime(rawUptimeUs); + mBatteryRealtime = mStats.getBatteryRealtime(rawRealtimeUs); + mTypeBatteryUptime = mStats.computeBatteryUptime(rawUptimeUs, mStatsType); + mTypeBatteryRealtime = mStats.computeBatteryRealtime(rawRealtimeUs, mStatsType); + mBatteryTimeRemaining = mStats.computeBatteryTimeRemaining(rawRealtimeUs); + mChargeTimeRemaining = mStats.computeChargeTimeRemaining(rawRealtimeUs); + + if (DEBUG) { + Log.d(TAG, "Raw time: realtime=" + (rawRealtimeUs/1000) + " uptime=" + + (rawUptimeUs/1000)); + Log.d(TAG, "Battery time: realtime=" + (mBatteryRealtime/1000) + " uptime=" + + (mBatteryUptime/1000)); + Log.d(TAG, "Battery type time: realtime=" + (mTypeBatteryRealtime/1000) + " uptime=" + + (mTypeBatteryUptime/1000)); + } + mMinDrainedPower = (mStats.getLowDischargeAmountSinceCharge() + * mPowerProfile.getBatteryCapacity()) / 100; + mMaxDrainedPower = (mStats.getHighDischargeAmountSinceCharge() + * mPowerProfile.getBatteryCapacity()) / 100; + + processAppUsage(); + + // Before aggregating apps in to users, collect all apps to sort by their ms per packet. + for (int i=0; i<mUsageList.size(); i++) { + BatterySipper bs = mUsageList.get(i); + bs.computeMobilemspp(); + if (bs.mobilemspp != 0) { + mMobilemsppList.add(bs); + } + } + for (int i=0; i<mUserSippers.size(); i++) { + List<BatterySipper> user = mUserSippers.valueAt(i); + for (int j=0; j<user.size(); j++) { + BatterySipper bs = user.get(j); + bs.computeMobilemspp(); + if (bs.mobilemspp != 0) { + mMobilemsppList.add(bs); + } + } + } + Collections.sort(mMobilemsppList, new Comparator<BatterySipper>() { + @Override + public int compare(BatterySipper lhs, BatterySipper rhs) { + if (lhs.mobilemspp < rhs.mobilemspp) { + return 1; + } else if (lhs.mobilemspp > rhs.mobilemspp) { + return -1; + } + return 0; + } + }); + + processMiscUsage(); + + if (DEBUG) { + Log.d(TAG, "Accuracy: total computed=" + makemAh(mComputedPower) + ", min discharge=" + + makemAh(mMinDrainedPower) + ", max discharge=" + makemAh(mMaxDrainedPower)); + } + mTotalPower = mComputedPower; + if (mStats.getLowDischargeAmountSinceCharge() > 1) { + if (mMinDrainedPower > mComputedPower) { + double amount = mMinDrainedPower - mComputedPower; + mTotalPower = mMinDrainedPower; + addEntryNoTotal(BatterySipper.DrainType.UNACCOUNTED, 0, amount); + } else if (mMaxDrainedPower < mComputedPower) { + double amount = mComputedPower - mMaxDrainedPower; + addEntryNoTotal(BatterySipper.DrainType.OVERCOUNTED, 0, amount); + } + } + + Collections.sort(mUsageList); + } + + private void processAppUsage() { + SensorManager sensorManager = (SensorManager) mContext.getSystemService( + Context.SENSOR_SERVICE); + final int which = mStatsType; + final int speedSteps = mPowerProfile.getNumSpeedSteps(); + final double[] powerCpuNormal = new double[speedSteps]; + final long[] cpuSpeedStepTimes = new long[speedSteps]; + for (int p = 0; p < speedSteps; p++) { + powerCpuNormal[p] = mPowerProfile.getAveragePower(PowerProfile.POWER_CPU_ACTIVE, p); + } + final double mobilePowerPerPacket = getMobilePowerPerPacket(); + final double mobilePowerPerMs = getMobilePowerPerMs(); + final double wifiPowerPerPacket = getWifiPowerPerPacket(); + long appWakelockTimeUs = 0; + BatterySipper osApp = null; + mStatsPeriod = mTypeBatteryRealtime; + SparseArray<? extends Uid> uidStats = mStats.getUidStats(); + final int NU = uidStats.size(); + for (int iu = 0; iu < NU; iu++) { + Uid u = uidStats.valueAt(iu); + double p; // in mAs + double power = 0; // in mAs + double highestDrain = 0; + String packageWithHighestDrain = null; + Map<String, ? extends BatteryStats.Uid.Proc> processStats = u.getProcessStats(); + long cpuTime = 0; + long cpuFgTime = 0; + long wakelockTime = 0; + long gpsTime = 0; + if (processStats.size() > 0) { + // Process CPU time + for (Map.Entry<String, ? extends BatteryStats.Uid.Proc> ent + : processStats.entrySet()) { + Uid.Proc ps = ent.getValue(); + final long userTime = ps.getUserTime(which); + final long systemTime = ps.getSystemTime(which); + final long foregroundTime = ps.getForegroundTime(which); + cpuFgTime += foregroundTime * 10; // convert to millis + final long tmpCpuTime = (userTime + systemTime) * 10; // convert to millis + int totalTimeAtSpeeds = 0; + // Get the total first + for (int step = 0; step < speedSteps; step++) { + cpuSpeedStepTimes[step] = ps.getTimeAtCpuSpeedStep(step, which); + totalTimeAtSpeeds += cpuSpeedStepTimes[step]; + } + if (totalTimeAtSpeeds == 0) totalTimeAtSpeeds = 1; + // Then compute the ratio of time spent at each speed + double processPower = 0; + for (int step = 0; step < speedSteps; step++) { + double ratio = (double) cpuSpeedStepTimes[step] / totalTimeAtSpeeds; + if (DEBUG && ratio != 0) Log.d(TAG, "UID " + u.getUid() + ": CPU step #" + + step + " ratio=" + makemAh(ratio) + " power=" + + makemAh(ratio*tmpCpuTime*powerCpuNormal[step] / (60*60*1000))); + processPower += ratio * tmpCpuTime * powerCpuNormal[step]; + } + cpuTime += tmpCpuTime; + if (DEBUG && processPower != 0) { + Log.d(TAG, String.format("process %s, cpu power=%s", + ent.getKey(), makemAh(processPower / (60*60*1000)))); + } + power += processPower; + if (packageWithHighestDrain == null + || packageWithHighestDrain.startsWith("*")) { + highestDrain = processPower; + packageWithHighestDrain = ent.getKey(); + } else if (highestDrain < processPower + && !ent.getKey().startsWith("*")) { + highestDrain = processPower; + packageWithHighestDrain = ent.getKey(); + } + } + } + if (cpuFgTime > cpuTime) { + if (DEBUG && cpuFgTime > cpuTime + 10000) { + Log.d(TAG, "WARNING! Cputime is more than 10 seconds behind Foreground time"); + } + cpuTime = cpuFgTime; // Statistics may not have been gathered yet. + } + power /= (60*60*1000); + + // Process wake lock usage + Map<String, ? extends BatteryStats.Uid.Wakelock> wakelockStats = u.getWakelockStats(); + for (Map.Entry<String, ? extends BatteryStats.Uid.Wakelock> wakelockEntry + : wakelockStats.entrySet()) { + Uid.Wakelock wakelock = wakelockEntry.getValue(); + // Only care about partial wake locks since full wake locks + // are canceled when the user turns the screen off. + BatteryStats.Timer timer = wakelock.getWakeTime(BatteryStats.WAKE_TYPE_PARTIAL); + if (timer != null) { + wakelockTime += timer.getTotalTimeLocked(mRawRealtime, which); + } + } + appWakelockTimeUs += wakelockTime; + wakelockTime /= 1000; // convert to millis + + // Add cost of holding a wake lock + p = (wakelockTime + * mPowerProfile.getAveragePower(PowerProfile.POWER_CPU_AWAKE)) / (60*60*1000); + if (DEBUG && p != 0) Log.d(TAG, "UID " + u.getUid() + ": wake " + + wakelockTime + " power=" + makemAh(p)); + power += p; + + // Add cost of mobile traffic + final long mobileRx = u.getNetworkActivityPackets(NETWORK_MOBILE_RX_DATA, mStatsType); + final long mobileTx = u.getNetworkActivityPackets(NETWORK_MOBILE_TX_DATA, mStatsType); + final long mobileRxB = u.getNetworkActivityBytes(NETWORK_MOBILE_RX_DATA, mStatsType); + final long mobileTxB = u.getNetworkActivityBytes(NETWORK_MOBILE_TX_DATA, mStatsType); + final long mobileActive = u.getMobileRadioActiveTime(mStatsType); + if (mobileActive > 0) { + // We are tracking when the radio is up, so can use the active time to + // determine power use. + mAppMobileActive += mobileActive; + p = (mobilePowerPerMs * mobileActive) / 1000; + } else { + // We are not tracking when the radio is up, so must approximate power use + // based on the number of packets. + p = (mobileRx + mobileTx) * mobilePowerPerPacket; + } + if (DEBUG && p != 0) Log.d(TAG, "UID " + u.getUid() + ": mobile packets " + + (mobileRx+mobileTx) + " active time " + mobileActive + + " power=" + makemAh(p)); + power += p; + + // Add cost of wifi traffic + final long wifiRx = u.getNetworkActivityPackets(NETWORK_WIFI_RX_DATA, mStatsType); + final long wifiTx = u.getNetworkActivityPackets(NETWORK_WIFI_TX_DATA, mStatsType); + final long wifiRxB = u.getNetworkActivityBytes(NETWORK_WIFI_RX_DATA, mStatsType); + final long wifiTxB = u.getNetworkActivityBytes(NETWORK_WIFI_TX_DATA, mStatsType); + p = (wifiRx + wifiTx) * wifiPowerPerPacket; + if (DEBUG && p != 0) Log.d(TAG, "UID " + u.getUid() + ": wifi packets " + + (mobileRx+mobileTx) + " power=" + makemAh(p)); + power += p; + + // Add cost of keeping WIFI running. + long wifiRunningTimeMs = u.getWifiRunningTime(mRawRealtime, which) / 1000; + mAppWifiRunning += wifiRunningTimeMs; + p = (wifiRunningTimeMs + * mPowerProfile.getAveragePower(PowerProfile.POWER_WIFI_ON)) / (60*60*1000); + if (DEBUG && p != 0) Log.d(TAG, "UID " + u.getUid() + ": wifi running " + + wifiRunningTimeMs + " power=" + makemAh(p)); + power += p; + + // Add cost of WIFI scans + long wifiScanTimeMs = u.getWifiScanTime(mRawRealtime, which) / 1000; + p = (wifiScanTimeMs + * mPowerProfile.getAveragePower(PowerProfile.POWER_WIFI_SCAN)) / (60*60*1000); + if (DEBUG) Log.d(TAG, "UID " + u.getUid() + ": wifi scan " + wifiScanTimeMs + + " power=" + makemAh(p)); + power += p; + for (int bin = 0; bin < BatteryStats.Uid.NUM_WIFI_BATCHED_SCAN_BINS; bin++) { + long batchScanTimeMs = u.getWifiBatchedScanTime(bin, mRawRealtime, which) / 1000; + p = ((batchScanTimeMs + * mPowerProfile.getAveragePower(PowerProfile.POWER_WIFI_BATCHED_SCAN, bin)) + ) / (60*60*1000); + if (DEBUG && p != 0) Log.d(TAG, "UID " + u.getUid() + ": wifi batched scan # " + bin + + " time=" + batchScanTimeMs + " power=" + makemAh(p)); + power += p; + } + + // Process Sensor usage + Map<Integer, ? extends BatteryStats.Uid.Sensor> sensorStats = u.getSensorStats(); + for (Map.Entry<Integer, ? extends BatteryStats.Uid.Sensor> sensorEntry + : sensorStats.entrySet()) { + Uid.Sensor sensor = sensorEntry.getValue(); + int sensorHandle = sensor.getHandle(); + BatteryStats.Timer timer = sensor.getSensorTime(); + long sensorTime = timer.getTotalTimeLocked(mRawRealtime, which) / 1000; + double multiplier = 0; + switch (sensorHandle) { + case Uid.Sensor.GPS: + multiplier = mPowerProfile.getAveragePower(PowerProfile.POWER_GPS_ON); + gpsTime = sensorTime; + break; + default: + List<Sensor> sensorList = sensorManager.getSensorList( + android.hardware.Sensor.TYPE_ALL); + for (android.hardware.Sensor s : sensorList) { + if (s.getHandle() == sensorHandle) { + multiplier = s.getPower(); + break; + } + } + } + p = (multiplier * sensorTime) / (60*60*1000); + if (DEBUG && p != 0) Log.d(TAG, "UID " + u.getUid() + ": sensor #" + sensorHandle + + " time=" + sensorTime + " power=" + makemAh(p)); + power += p; + } + + if (DEBUG && power != 0) Log.d(TAG, String.format("UID %d: total power=%s", + u.getUid(), makemAh(power))); + + // Add the app to the list if it is consuming power + final int userId = UserHandle.getUserId(u.getUid()); + if (power != 0 || u.getUid() == 0) { + BatterySipper app = new BatterySipper(BatterySipper.DrainType.APP, u, + new double[] {power}); + app.cpuTime = cpuTime; + app.gpsTime = gpsTime; + app.wifiRunningTime = wifiRunningTimeMs; + app.cpuFgTime = cpuFgTime; + app.wakeLockTime = wakelockTime; + app.mobileRxPackets = mobileRx; + app.mobileTxPackets = mobileTx; + app.mobileActive = mobileActive / 1000; + app.mobileActiveCount = u.getMobileRadioActiveCount(mStatsType); + app.wifiRxPackets = wifiRx; + app.wifiTxPackets = wifiTx; + app.mobileRxBytes = mobileRxB; + app.mobileTxBytes = mobileTxB; + app.wifiRxBytes = wifiRxB; + app.wifiTxBytes = wifiTxB; + app.packageWithHighestDrain = packageWithHighestDrain; + if (u.getUid() == Process.WIFI_UID) { + mWifiSippers.add(app); + mWifiPower += power; + } else if (u.getUid() == Process.BLUETOOTH_UID) { + mBluetoothSippers.add(app); + mBluetoothPower += power; + } else if (mAsUser != UserHandle.USER_ALL && userId != mAsUser + && UserHandle.getAppId(u.getUid()) >= Process.FIRST_APPLICATION_UID) { + List<BatterySipper> list = mUserSippers.get(userId); + if (list == null) { + list = new ArrayList<BatterySipper>(); + mUserSippers.put(userId, list); + } + list.add(app); + if (power != 0) { + Double userPower = mUserPower.get(userId); + if (userPower == null) { + userPower = power; + } else { + userPower += power; + } + mUserPower.put(userId, userPower); + } + } else { + mUsageList.add(app); + if (power > mMaxPower) mMaxPower = power; + mComputedPower += power; + } + if (u.getUid() == 0) { + osApp = app; + } + } + } + + // The device has probably been awake for longer than the screen on + // time and application wake lock time would account for. Assign + // this remainder to the OS, if possible. + if (osApp != null) { + long wakeTimeMillis = mBatteryUptime / 1000; + wakeTimeMillis -= (appWakelockTimeUs / 1000) + + (mStats.getScreenOnTime(mRawRealtime, which) / 1000); + if (wakeTimeMillis > 0) { + double power = (wakeTimeMillis + * mPowerProfile.getAveragePower(PowerProfile.POWER_CPU_AWAKE)) + / (60*60*1000); + if (DEBUG) Log.d(TAG, "OS wakeLockTime " + wakeTimeMillis + " power " + + makemAh(power)); + osApp.wakeLockTime += wakeTimeMillis; + osApp.value += power; + osApp.values[0] += power; + if (osApp.value > mMaxPower) mMaxPower = osApp.value; + mComputedPower += power; + } + } + } + + private void addPhoneUsage() { + long phoneOnTimeMs = mStats.getPhoneOnTime(mRawRealtime, mStatsType) / 1000; + double phoneOnPower = mPowerProfile.getAveragePower(PowerProfile.POWER_RADIO_ACTIVE) + * phoneOnTimeMs / (60*60*1000); + if (phoneOnPower != 0) { + BatterySipper bs = addEntry(BatterySipper.DrainType.PHONE, phoneOnTimeMs, phoneOnPower); + } + } + + private void addScreenUsage() { + double power = 0; + long screenOnTimeMs = mStats.getScreenOnTime(mRawRealtime, mStatsType) / 1000; + power += screenOnTimeMs * mPowerProfile.getAveragePower(PowerProfile.POWER_SCREEN_ON); + final double screenFullPower = + mPowerProfile.getAveragePower(PowerProfile.POWER_SCREEN_FULL); + for (int i = 0; i < BatteryStats.NUM_SCREEN_BRIGHTNESS_BINS; i++) { + double screenBinPower = screenFullPower * (i + 0.5f) + / BatteryStats.NUM_SCREEN_BRIGHTNESS_BINS; + long brightnessTime = mStats.getScreenBrightnessTime(i, mRawRealtime, mStatsType) + / 1000; + double p = screenBinPower*brightnessTime; + if (DEBUG && p != 0) { + Log.d(TAG, "Screen bin #" + i + ": time=" + brightnessTime + + " power=" + makemAh(p / (60 * 60 * 1000))); + } + power += p; + } + power /= (60*60*1000); // To hours + if (power != 0) { + addEntry(BatterySipper.DrainType.SCREEN, screenOnTimeMs, power); + } + } + + private void addRadioUsage() { + double power = 0; + final int BINS = SignalStrength.NUM_SIGNAL_STRENGTH_BINS; + long signalTimeMs = 0; + long noCoverageTimeMs = 0; + for (int i = 0; i < BINS; i++) { + long strengthTimeMs = mStats.getPhoneSignalStrengthTime(i, mRawRealtime, mStatsType) + / 1000; + double p = (strengthTimeMs * mPowerProfile.getAveragePower(PowerProfile.POWER_RADIO_ON, i)) + / (60*60*1000); + if (DEBUG && p != 0) { + Log.d(TAG, "Cell strength #" + i + ": time=" + strengthTimeMs + " power=" + + makemAh(p)); + } + power += p; + signalTimeMs += strengthTimeMs; + if (i == 0) { + noCoverageTimeMs = strengthTimeMs; + } + } + long scanningTimeMs = mStats.getPhoneSignalScanningTime(mRawRealtime, mStatsType) + / 1000; + double p = (scanningTimeMs * mPowerProfile.getAveragePower( + PowerProfile.POWER_RADIO_SCANNING)) + / (60*60*1000); + if (DEBUG && p != 0) { + Log.d(TAG, "Cell radio scanning: time=" + scanningTimeMs + " power=" + makemAh(p)); + } + power += p; + long radioActiveTimeUs = mStats.getMobileRadioActiveTime(mRawRealtime, mStatsType); + long remainingActiveTime = (radioActiveTimeUs - mAppMobileActive) / 1000; + if (remainingActiveTime > 0) { + power += getMobilePowerPerMs() * remainingActiveTime; + } + if (power != 0) { + BatterySipper bs = + addEntry(BatterySipper.DrainType.CELL, signalTimeMs, power); + if (signalTimeMs != 0) { + bs.noCoveragePercent = noCoverageTimeMs * 100.0 / signalTimeMs; + } + bs.mobileActive = remainingActiveTime; + bs.mobileActiveCount = mStats.getMobileRadioActiveUnknownCount(mStatsType); + } + } + + private void aggregateSippers(BatterySipper bs, List<BatterySipper> from, String tag) { + for (int i=0; i<from.size(); i++) { + BatterySipper wbs = from.get(i); + if (DEBUG) Log.d(TAG, tag + " adding sipper " + wbs + ": cpu=" + wbs.cpuTime); + bs.cpuTime += wbs.cpuTime; + bs.gpsTime += wbs.gpsTime; + bs.wifiRunningTime += wbs.wifiRunningTime; + bs.cpuFgTime += wbs.cpuFgTime; + bs.wakeLockTime += wbs.wakeLockTime; + bs.mobileRxPackets += wbs.mobileRxPackets; + bs.mobileTxPackets += wbs.mobileTxPackets; + bs.mobileActive += wbs.mobileActive; + bs.mobileActiveCount += wbs.mobileActiveCount; + bs.wifiRxPackets += wbs.wifiRxPackets; + bs.wifiTxPackets += wbs.wifiTxPackets; + bs.mobileRxBytes += wbs.mobileRxBytes; + bs.mobileTxBytes += wbs.mobileTxBytes; + bs.wifiRxBytes += wbs.wifiRxBytes; + bs.wifiTxBytes += wbs.wifiTxBytes; + } + bs.computeMobilemspp(); + } + + private void addWiFiUsage() { + long onTimeMs = mStats.getWifiOnTime(mRawRealtime, mStatsType) / 1000; + long runningTimeMs = mStats.getGlobalWifiRunningTime(mRawRealtime, mStatsType) / 1000; + if (DEBUG) Log.d(TAG, "WIFI runningTime=" + runningTimeMs + + " app runningTime=" + mAppWifiRunning); + runningTimeMs -= mAppWifiRunning; + if (runningTimeMs < 0) runningTimeMs = 0; + double wifiPower = (onTimeMs * 0 /* TODO */ + * mPowerProfile.getAveragePower(PowerProfile.POWER_WIFI_ON) + + runningTimeMs * mPowerProfile.getAveragePower(PowerProfile.POWER_WIFI_ON)) + / (60*60*1000); + if (DEBUG && wifiPower != 0) { + Log.d(TAG, "Wifi: time=" + runningTimeMs + " power=" + makemAh(wifiPower)); + } + if ((wifiPower+mWifiPower) != 0) { + BatterySipper bs = addEntry(BatterySipper.DrainType.WIFI, runningTimeMs, + wifiPower + mWifiPower); + aggregateSippers(bs, mWifiSippers, "WIFI"); + } + } + + private void addIdleUsage() { + long idleTimeMs = (mTypeBatteryRealtime + - mStats.getScreenOnTime(mRawRealtime, mStatsType)) / 1000; + double idlePower = (idleTimeMs * mPowerProfile.getAveragePower(PowerProfile.POWER_CPU_IDLE)) + / (60*60*1000); + if (DEBUG && idlePower != 0) { + Log.d(TAG, "Idle: time=" + idleTimeMs + " power=" + makemAh(idlePower)); + } + if (idlePower != 0) { + addEntry(BatterySipper.DrainType.IDLE, idleTimeMs, idlePower); + } + } + + private void addBluetoothUsage() { + long btOnTimeMs = mStats.getBluetoothOnTime(mRawRealtime, mStatsType) / 1000; + double btPower = btOnTimeMs * mPowerProfile.getAveragePower(PowerProfile.POWER_BLUETOOTH_ON) + / (60*60*1000); + if (DEBUG && btPower != 0) { + Log.d(TAG, "Bluetooth: time=" + btOnTimeMs + " power=" + makemAh(btPower)); + } + int btPingCount = mStats.getBluetoothPingCount(); + double pingPower = (btPingCount + * mPowerProfile.getAveragePower(PowerProfile.POWER_BLUETOOTH_AT_CMD)) + / (60*60*1000); + if (DEBUG && pingPower != 0) { + Log.d(TAG, "Bluetooth ping: count=" + btPingCount + " power=" + makemAh(pingPower)); + } + btPower += pingPower; + if ((btPower+mBluetoothPower) != 0) { + BatterySipper bs = addEntry(BatterySipper.DrainType.BLUETOOTH, btOnTimeMs, + btPower + mBluetoothPower); + aggregateSippers(bs, mBluetoothSippers, "Bluetooth"); + } + } + + private void addUserUsage() { + for (int i=0; i<mUserSippers.size(); i++) { + final int userId = mUserSippers.keyAt(i); + final List<BatterySipper> sippers = mUserSippers.valueAt(i); + Double userPower = mUserPower.get(userId); + double power = (userPower != null) ? userPower : 0.0; + BatterySipper bs = addEntry(BatterySipper.DrainType.USER, 0, power); + bs.userId = userId; + aggregateSippers(bs, sippers, "User"); + } + } + + /** + * Return estimated power (in mAs) of sending or receiving a packet with the mobile radio. + */ + private double getMobilePowerPerPacket() { + final long MOBILE_BPS = 200000; // TODO: Extract average bit rates from system + final double MOBILE_POWER = mPowerProfile.getAveragePower(PowerProfile.POWER_RADIO_ACTIVE) + / 3600; + + final long mobileRx = mStats.getNetworkActivityPackets(NETWORK_MOBILE_RX_DATA, mStatsType); + final long mobileTx = mStats.getNetworkActivityPackets(NETWORK_MOBILE_TX_DATA, mStatsType); + final long mobileData = mobileRx + mobileTx; + + final long radioDataUptimeMs + = mStats.getMobileRadioActiveTime(mRawRealtime, mStatsType) / 1000; + final double mobilePps = (mobileData != 0 && radioDataUptimeMs != 0) + ? (mobileData / (double)radioDataUptimeMs) + : (((double)MOBILE_BPS) / 8 / 2048); + + return (MOBILE_POWER / mobilePps) / (60*60); + } + + /** + * Return estimated power (in mAs) of keeping the radio up + */ + private double getMobilePowerPerMs() { + return mPowerProfile.getAveragePower(PowerProfile.POWER_RADIO_ACTIVE) / (60*60*1000); + } + + /** + * Return estimated power (in mAs) of sending a byte with the Wi-Fi radio. + */ + private double getWifiPowerPerPacket() { + final long WIFI_BPS = 1000000; // TODO: Extract average bit rates from system + final double WIFI_POWER = mPowerProfile.getAveragePower(PowerProfile.POWER_WIFI_ACTIVE) + / 3600; + return (WIFI_POWER / (((double)WIFI_BPS) / 8 / 2048)) / (60*60); + } + + private void processMiscUsage() { + addUserUsage(); + addPhoneUsage(); + addScreenUsage(); + addWiFiUsage(); + addBluetoothUsage(); + addIdleUsage(); // Not including cellular idle power + // Don't compute radio usage if it's a wifi-only device + ConnectivityManager cm = (ConnectivityManager)mContext.getSystemService( + Context.CONNECTIVITY_SERVICE); + if (cm.isNetworkSupported(ConnectivityManager.TYPE_MOBILE)) { + addRadioUsage(); + } + } + + private BatterySipper addEntry(DrainType drainType, long time, double power) { + mComputedPower += power; + return addEntryNoTotal(drainType, time, power); + } + + private BatterySipper addEntryNoTotal(DrainType drainType, long time, double power) { + if (power > mMaxPower) mMaxPower = power; + BatterySipper bs = new BatterySipper(drainType, null, new double[] {power}); + bs.usageTime = time; + mUsageList.add(bs); + return bs; + } + + public List<BatterySipper> getUsageList() { + return mUsageList; + } + + public List<BatterySipper> getMobilemsppList() { + return mMobilemsppList; + } + + public long getStatsPeriod() { return mStatsPeriod; } + + public int getStatsType() { return mStatsType; }; + + public double getMaxPower() { return mMaxPower; } + + public double getTotalPower() { return mTotalPower; } + + public double getComputedPower() { return mComputedPower; } + + public double getMinDrainedPower() { + return mMinDrainedPower; + } + + public double getMaxDrainedPower() { + return mMaxDrainedPower; + } + + public long getBatteryTimeRemaining() { return mBatteryTimeRemaining; } + + public long getChargeTimeRemaining() { return mChargeTimeRemaining; } + + private void load() { + if (mBatteryInfo == null) { + return; + } + try { + byte[] data = mBatteryInfo.getStatistics(); + Parcel parcel = Parcel.obtain(); + parcel.unmarshall(data, 0, data.length); + parcel.setDataPosition(0); + BatteryStatsImpl stats = com.android.internal.os.BatteryStatsImpl.CREATOR + .createFromParcel(parcel); + stats.distributeWorkLocked(BatteryStats.STATS_SINCE_CHARGED); + mStats = stats; + } catch (RemoteException e) { + Log.e(TAG, "RemoteException:", e); + } + if (mCollectBatteryBroadcast) { + mBatteryBroadcast = mContext.registerReceiver(null, + new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); + } + } +} diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java index 8728610..f63fa8a 100644 --- a/core/java/com/android/internal/os/BatteryStatsImpl.java +++ b/core/java/com/android/internal/os/BatteryStatsImpl.java @@ -16,12 +16,15 @@ package com.android.internal.os; +import static android.net.NetworkStats.UID_ALL; import static com.android.server.NetworkManagementSocketTagger.PROP_QTAGUID_ENABLED; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHeadset; +import android.content.Context; import android.net.ConnectivityManager; import android.net.NetworkStats; +import android.os.BadParcelableException; import android.os.BatteryManager; import android.os.BatteryStats; import android.os.FileUtils; @@ -35,6 +38,7 @@ import android.os.Process; import android.os.SystemClock; import android.os.SystemProperties; import android.os.WorkSource; +import android.telephony.DataConnectionRealTimeInfo; import android.telephony.ServiceState; import android.telephony.SignalStrength; import android.telephony.TelephonyManager; @@ -44,24 +48,23 @@ import android.util.PrintWriterPrinter; import android.util.Printer; import android.util.Slog; import android.util.SparseArray; +import android.util.SparseBooleanArray; +import android.util.SparseIntArray; import android.util.TimeUtils; import com.android.internal.annotations.GuardedBy; import com.android.internal.net.NetworkStatsFactory; +import com.android.internal.util.ArrayUtils; import com.android.internal.util.FastPrintWriter; import com.android.internal.util.JournaledFile; -import com.google.android.collect.Sets; -import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; -import java.io.FileReader; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -85,7 +88,7 @@ public final class BatteryStatsImpl extends BatteryStats { private static final int MAGIC = 0xBA757475; // 'BATSTATS' // Current on-disk Parcel version - private static final int VERSION = 67 + (USE_OLD_HISTORY ? 1000 : 0); + private static final int VERSION = 104 + (USE_OLD_HISTORY ? 1000 : 0); // Maximum number of items we will record in the history. private static final int MAX_HISTORY_ITEMS = 2000; @@ -141,6 +144,11 @@ public final class BatteryStatsImpl extends BatteryStats { private BatteryCallback mCallback; /** + * Mapping isolated uids to the actual owning app uid. + */ + final SparseIntArray mIsolatedUids = new SparseIntArray(); + + /** * The statistics we have collected organized by uids. */ final SparseArray<BatteryStatsImpl.Uid> mUidStats = @@ -167,13 +175,24 @@ public final class BatteryStatsImpl extends BatteryStats { // These are the objects that will want to do something when the device // is unplugged from power. - final ArrayList<Unpluggable> mUnpluggables = new ArrayList<Unpluggable>(); + final TimeBase mOnBatteryTimeBase = new TimeBase(); + + // These are the objects that will want to do something when the device + // is unplugged from power *and* the screen is off. + final TimeBase mOnBatteryScreenOffTimeBase = new TimeBase(); + + // Set to true when we want to distribute CPU across wakelocks for the next + // CPU update, even if we aren't currently running wake locks. + boolean mDistributeWakelockCpu; boolean mShuttingDown; + HashMap<String, SparseBooleanArray>[] mActiveEvents + = (HashMap<String, SparseBooleanArray>[]) new HashMap[HistoryItem.EVENT_COUNT]; + long mHistoryBaseTime; boolean mHaveBatteryLevel = false; - boolean mRecordingHistory = true; + boolean mRecordingHistory = false; int mNumHistoryItems; static final int MAX_HISTORY_BUFFER = 128*1024; // 128KB @@ -182,9 +201,18 @@ public final class BatteryStatsImpl extends BatteryStats { final HistoryItem mHistoryLastWritten = new HistoryItem(); final HistoryItem mHistoryLastLastWritten = new HistoryItem(); final HistoryItem mHistoryReadTmp = new HistoryItem(); + final HistoryItem mHistoryAddTmp = new HistoryItem(); + final HashMap<HistoryTag, Integer> mHistoryTagPool = new HashMap<HistoryTag, Integer>(); + String[] mReadHistoryStrings; + int[] mReadHistoryUids; + int mReadHistoryChars; + int mNextHistoryTagIdx = 0; + int mNumHistoryTagChars = 0; int mHistoryBufferLastPos = -1; boolean mHistoryOverflow = false; - long mLastHistoryTime = 0; + long mLastHistoryElapsedRealtime = 0; + long mTrackRunningHistoryElapsedRealtime = 0; + long mTrackRunningHistoryUptime = 0; final HistoryItem mHistoryCur = new HistoryItem(); @@ -199,17 +227,15 @@ public final class BatteryStatsImpl extends BatteryStats { int mStartCount; - long mBatteryUptime; - long mBatteryLastUptime; - long mBatteryRealtime; - long mBatteryLastRealtime; + long mStartClockTime; long mUptime; long mUptimeStart; - long mLastUptime; long mRealtime; long mRealtimeStart; - long mLastRealtime; + + int mWakeLockNesting; + boolean mWakeLockImportant; boolean mScreenOn; StopwatchTimer mScreenOnTimer; @@ -239,19 +265,33 @@ public final class BatteryStatsImpl extends BatteryStats { final StopwatchTimer[] mPhoneDataConnectionsTimer = new StopwatchTimer[NUM_DATA_CONNECTION_TYPES]; - final LongSamplingCounter[] mNetworkActivityCounters = + final LongSamplingCounter[] mNetworkByteActivityCounters = + new LongSamplingCounter[NUM_NETWORK_ACTIVITY_TYPES]; + final LongSamplingCounter[] mNetworkPacketActivityCounters = new LongSamplingCounter[NUM_NETWORK_ACTIVITY_TYPES]; boolean mWifiOn; StopwatchTimer mWifiOnTimer; - int mWifiOnUid = -1; boolean mGlobalWifiRunning; StopwatchTimer mGlobalWifiRunningTimer; + int mWifiState = -1; + final StopwatchTimer[] mWifiStateTimer = new StopwatchTimer[NUM_WIFI_STATES]; + boolean mBluetoothOn; StopwatchTimer mBluetoothOnTimer; + int mBluetoothState = -1; + final StopwatchTimer[] mBluetoothStateTimer = new StopwatchTimer[NUM_BLUETOOTH_STATES]; + + int mMobileRadioPowerState = DataConnectionRealTimeInfo.DC_POWER_STATE_LOW; + StopwatchTimer mMobileRadioActiveTimer; + StopwatchTimer mMobileRadioActivePerAppTimer; + LongSamplingCounter mMobileRadioActiveAdjustedTime; + LongSamplingCounter mMobileRadioActiveUnknownTime; + LongSamplingCounter mMobileRadioActiveUnknownCount; + /** Bluetooth headset object */ BluetoothHeadset mBtHeadset; @@ -261,20 +301,15 @@ public final class BatteryStatsImpl extends BatteryStats { */ boolean mOnBattery; boolean mOnBatteryInternal; - long mTrackBatteryPastUptime; - long mTrackBatteryUptimeStart; - long mTrackBatteryPastRealtime; - long mTrackBatteryRealtimeStart; - - long mUnpluggedBatteryUptime; - long mUnpluggedBatteryRealtime; /* * These keep track of battery levels (1-100) at the last plug event and the last unplug event. */ int mDischargeStartLevel; int mDischargeUnplugLevel; + int mDischargePlugLevel; int mDischargeCurrentLevel; + int mCurrentBatteryLevel; int mLowDischargeAmountSinceCharge; int mHighDischargeAmountSinceCharge; int mDischargeScreenOnUnplugLevel; @@ -284,10 +319,19 @@ public final class BatteryStatsImpl extends BatteryStats { int mDischargeAmountScreenOff; int mDischargeAmountScreenOffSinceCharge; - long mLastWriteTime = 0; // Milliseconds + static final int MAX_LEVEL_STEPS = 100; - private long mRadioDataUptime; - private long mRadioDataStart; + int mLastDischargeStepLevel; + long mLastDischargeStepTime; + int mNumDischargeStepDurations; + final long[] mDischargeStepDurations = new long[MAX_LEVEL_STEPS]; + + int mLastChargeStepLevel; + long mLastChargeStepTime; + int mNumChargeStepDurations; + final long[] mChargeStepDurations = new long[MAX_LEVEL_STEPS]; + + long mLastWriteTime = 0; // Milliseconds private int mBluetoothPingCount; private int mBluetoothPingStart = -1; @@ -302,12 +346,21 @@ public final class BatteryStatsImpl extends BatteryStats { private final HashMap<String, SamplingTimer> mKernelWakelockStats = new HashMap<String, SamplingTimer>(); - public Map<String, ? extends SamplingTimer> getKernelWakelockStats() { + public Map<String, ? extends Timer> getKernelWakelockStats() { return mKernelWakelockStats; } private static int sKernelWakelockUpdateVersion = 0; + String mLastWakeupReason = null; + long mLastWakeupUptimeMs = 0; + private final HashMap<String, LongSamplingCounter> mWakeupReasonStats = + new HashMap<String, LongSamplingCounter>(); + + public Map<String, ? extends LongCounter> getWakeupReasonStats() { + return mWakeupReasonStats; + } + private static final int[] PROC_WAKELOCKS_FORMAT = new int[] { Process.PROC_TAB_TERM|Process.PROC_OUT_STRING| // 0: name Process.PROC_QUOTES, @@ -340,15 +393,18 @@ public final class BatteryStatsImpl extends BatteryStats { private final Map<String, KernelWakelockStats> mProcWakelockFileStats = new HashMap<String, KernelWakelockStats>(); - private HashMap<String, Integer> mUidCache = new HashMap<String, Integer>(); - private final NetworkStatsFactory mNetworkStatsFactory = new NetworkStatsFactory(); - private NetworkStats mLastSnapshot; + private NetworkStats mCurMobileSnapshot = new NetworkStats(SystemClock.elapsedRealtime(), 50); + private NetworkStats mLastMobileSnapshot = new NetworkStats(SystemClock.elapsedRealtime(), 50); + private NetworkStats mCurWifiSnapshot = new NetworkStats(SystemClock.elapsedRealtime(), 50); + private NetworkStats mLastWifiSnapshot = new NetworkStats(SystemClock.elapsedRealtime(), 50); + private NetworkStats mTmpNetworkStats; + private final NetworkStats.Entry mTmpNetworkStatsEntry = new NetworkStats.Entry(); @GuardedBy("this") - private HashSet<String> mMobileIfaces = Sets.newHashSet(); + private String[] mMobileIfaces = new String[0]; @GuardedBy("this") - private HashSet<String> mWifiIfaces = Sets.newHashSet(); + private String[] mWifiIfaces = new String[0]; // For debugging public BatteryStatsImpl() { @@ -356,35 +412,228 @@ public final class BatteryStatsImpl extends BatteryStats { mHandler = null; } - public static interface Unpluggable { - void unplug(long elapsedRealtime, long batteryUptime, long batteryRealtime); - void plug(long elapsedRealtime, long batteryUptime, long batteryRealtime); + public static interface TimeBaseObs { + void onTimeStarted(long elapsedRealtime, long baseUptime, long baseRealtime); + void onTimeStopped(long elapsedRealtime, long baseUptime, long baseRealtime); + } + + static class TimeBase { + private final ArrayList<TimeBaseObs> mObservers = new ArrayList<TimeBaseObs>(); + + private long mUptime; + private long mRealtime; + + private boolean mRunning; + + private long mPastUptime; + private long mUptimeStart; + private long mPastRealtime; + private long mRealtimeStart; + private long mUnpluggedUptime; + private long mUnpluggedRealtime; + + public void dump(PrintWriter pw, String prefix) { + StringBuilder sb = new StringBuilder(128); + pw.print(prefix); pw.print("mRunning="); pw.println(mRunning); + sb.setLength(0); + sb.append(prefix); + sb.append("mUptime="); + formatTimeMs(sb, mUptime / 1000); + pw.println(sb.toString()); + sb.setLength(0); + sb.append(prefix); + sb.append("mRealtime="); + formatTimeMs(sb, mRealtime / 1000); + pw.println(sb.toString()); + sb.setLength(0); + sb.append(prefix); + sb.append("mPastUptime="); + formatTimeMs(sb, mPastUptime / 1000); sb.append("mUptimeStart="); + formatTimeMs(sb, mUptimeStart / 1000); + sb.append("mUnpluggedUptime="); formatTimeMs(sb, mUnpluggedUptime / 1000); + pw.println(sb.toString()); + sb.setLength(0); + sb.append(prefix); + sb.append("mPastRealtime="); + formatTimeMs(sb, mPastRealtime / 1000); sb.append("mRealtimeStart="); + formatTimeMs(sb, mRealtimeStart / 1000); + sb.append("mUnpluggedRealtime="); formatTimeMs(sb, mUnpluggedRealtime / 1000); + pw.println(sb.toString()); + } + + public void add(TimeBaseObs observer) { + mObservers.add(observer); + } + + public void remove(TimeBaseObs observer) { + if (!mObservers.remove(observer)) { + Slog.wtf(TAG, "Removed unknown observer: " + observer); + } + } + + public void init(long uptime, long realtime) { + mRealtime = 0; + mUptime = 0; + mPastUptime = 0; + mPastRealtime = 0; + mUptimeStart = uptime; + mRealtimeStart = realtime; + mUnpluggedUptime = getUptime(mUptimeStart); + mUnpluggedRealtime = getRealtime(mRealtimeStart); + } + + public void reset(long uptime, long realtime) { + if (!mRunning) { + mPastUptime = 0; + mPastRealtime = 0; + } else { + mUptimeStart = uptime; + mRealtimeStart = realtime; + mUnpluggedUptime = getUptime(uptime); + mUnpluggedRealtime = getRealtime(realtime); + } + } + + public long computeUptime(long curTime, int which) { + switch (which) { + case STATS_SINCE_CHARGED: + return mUptime + getUptime(curTime); + case STATS_CURRENT: + return getUptime(curTime); + case STATS_SINCE_UNPLUGGED: + return getUptime(curTime) - mUnpluggedUptime; + } + return 0; + } + + public long computeRealtime(long curTime, int which) { + switch (which) { + case STATS_SINCE_CHARGED: + return mRealtime + getRealtime(curTime); + case STATS_CURRENT: + return getRealtime(curTime); + case STATS_SINCE_UNPLUGGED: + return getRealtime(curTime) - mUnpluggedRealtime; + } + return 0; + } + + public long getUptime(long curTime) { + long time = mPastUptime; + if (mRunning) { + time += curTime - mUptimeStart; + } + return time; + } + + public long getRealtime(long curTime) { + long time = mPastRealtime; + if (mRunning) { + time += curTime - mRealtimeStart; + } + return time; + } + + public long getUptimeStart() { + return mUptimeStart; + } + + public long getRealtimeStart() { + return mRealtimeStart; + } + + public boolean isRunning() { + return mRunning; + } + + public boolean setRunning(boolean running, long uptime, long realtime) { + if (mRunning != running) { + mRunning = running; + if (running) { + mUptimeStart = uptime; + mRealtimeStart = realtime; + long batteryUptime = mUnpluggedUptime = getUptime(uptime); + long batteryRealtime = mUnpluggedRealtime = getRealtime(realtime); + + for (int i = mObservers.size() - 1; i >= 0; i--) { + mObservers.get(i).onTimeStarted(realtime, batteryUptime, batteryRealtime); + } + } else { + mPastUptime += uptime - mUptimeStart; + mPastRealtime += realtime - mRealtimeStart; + + long batteryUptime = getUptime(uptime); + long batteryRealtime = getRealtime(realtime); + + for (int i = mObservers.size() - 1; i >= 0; i--) { + mObservers.get(i).onTimeStopped(realtime, batteryUptime, batteryRealtime); + } + } + return true; + } + return false; + } + + public void readSummaryFromParcel(Parcel in) { + mUptime = in.readLong(); + mRealtime = in.readLong(); + } + + public void writeSummaryToParcel(Parcel out, long uptime, long realtime) { + out.writeLong(computeUptime(uptime, STATS_SINCE_CHARGED)); + out.writeLong(computeRealtime(realtime, STATS_SINCE_CHARGED)); + } + + public void readFromParcel(Parcel in) { + mRunning = false; + mUptime = in.readLong(); + mPastUptime = in.readLong(); + mUptimeStart = in.readLong(); + mRealtime = in.readLong(); + mPastRealtime = in.readLong(); + mRealtimeStart = in.readLong(); + mUnpluggedUptime = in.readLong(); + mUnpluggedRealtime = in.readLong(); + } + + public void writeToParcel(Parcel out, long uptime, long realtime) { + final long runningUptime = getUptime(uptime); + final long runningRealtime = getRealtime(realtime); + out.writeLong(mUptime); + out.writeLong(runningUptime); + out.writeLong(mUptimeStart); + out.writeLong(mRealtime); + out.writeLong(runningRealtime); + out.writeLong(mRealtimeStart); + out.writeLong(mUnpluggedUptime); + out.writeLong(mUnpluggedRealtime); + } } /** * State for keeping track of counting information. */ - public static class Counter extends BatteryStats.Counter implements Unpluggable { + public static class Counter extends BatteryStats.Counter implements TimeBaseObs { final AtomicInteger mCount = new AtomicInteger(); - final ArrayList<Unpluggable> mUnpluggables; + final TimeBase mTimeBase; int mLoadedCount; int mLastCount; int mUnpluggedCount; int mPluggedCount; - Counter(ArrayList<Unpluggable> unpluggables, Parcel in) { - mUnpluggables = unpluggables; + Counter(TimeBase timeBase, Parcel in) { + mTimeBase = timeBase; mPluggedCount = in.readInt(); mCount.set(mPluggedCount); mLoadedCount = in.readInt(); mLastCount = 0; mUnpluggedCount = in.readInt(); - unpluggables.add(this); + timeBase.add(this); } - Counter(ArrayList<Unpluggable> unpluggables) { - mUnpluggables = unpluggables; - unpluggables.add(this); + Counter(TimeBase timeBase) { + mTimeBase = timeBase; + timeBase.add(this); } public void writeToParcel(Parcel out) { @@ -393,12 +642,12 @@ public final class BatteryStatsImpl extends BatteryStats { out.writeInt(mUnpluggedCount); } - public void unplug(long elapsedRealtime, long batteryUptime, long batteryRealtime) { + public void onTimeStarted(long elapsedRealtime, long baseUptime, long baseRealtime) { mUnpluggedCount = mPluggedCount; mCount.set(mPluggedCount); } - public void plug(long elapsedRealtime, long batteryUptime, long batteryRealtime) { + public void onTimeStopped(long elapsedRealtime, long baseUptime, long baseRealtime) { mPluggedCount = mCount.get(); } @@ -420,16 +669,11 @@ public final class BatteryStatsImpl extends BatteryStats { @Override public int getCountLocked(int which) { - int val; - if (which == STATS_LAST) { - val = mLastCount; - } else { - val = mCount.get(); - if (which == STATS_SINCE_UNPLUGGED) { - val -= mUnpluggedCount; - } else if (which != STATS_SINCE_CHARGED) { - val -= mLoadedCount; - } + int val = mCount.get(); + if (which == STATS_SINCE_UNPLUGGED) { + val -= mUnpluggedCount; + } else if (which != STATS_SINCE_CHARGED) { + val -= mLoadedCount; } return val; @@ -458,7 +702,7 @@ public final class BatteryStatsImpl extends BatteryStats { } void detach() { - mUnpluggables.remove(this); + mTimeBase.remove(this); } void writeSummaryFromParcelLocked(Parcel out) { @@ -475,12 +719,12 @@ public final class BatteryStatsImpl extends BatteryStats { } public static class SamplingCounter extends Counter { - SamplingCounter(ArrayList<Unpluggable> unpluggables, Parcel in) { - super(unpluggables, in); + SamplingCounter(TimeBase timeBase, Parcel in) { + super(timeBase, in); } - SamplingCounter(ArrayList<Unpluggable> unpluggables) { - super(unpluggables); + SamplingCounter(TimeBase timeBase) { + super(timeBase); } public void addCountAtomic(long count) { @@ -488,27 +732,27 @@ public final class BatteryStatsImpl extends BatteryStats { } } - public static class LongSamplingCounter implements Unpluggable { - final ArrayList<Unpluggable> mUnpluggables; + public static class LongSamplingCounter extends LongCounter implements TimeBaseObs { + final TimeBase mTimeBase; long mCount; long mLoadedCount; long mLastCount; long mUnpluggedCount; long mPluggedCount; - LongSamplingCounter(ArrayList<Unpluggable> unpluggables, Parcel in) { - mUnpluggables = unpluggables; + LongSamplingCounter(TimeBase timeBase, Parcel in) { + mTimeBase = timeBase; mPluggedCount = in.readLong(); mCount = mPluggedCount; mLoadedCount = in.readLong(); mLastCount = 0; mUnpluggedCount = in.readLong(); - unpluggables.add(this); + timeBase.add(this); } - LongSamplingCounter(ArrayList<Unpluggable> unpluggables) { - mUnpluggables = unpluggables; - unpluggables.add(this); + LongSamplingCounter(TimeBase timeBase) { + mTimeBase = timeBase; + timeBase.add(this); } public void writeToParcel(Parcel out) { @@ -518,32 +762,35 @@ public final class BatteryStatsImpl extends BatteryStats { } @Override - public void unplug(long elapsedRealtime, long batteryUptime, long batteryRealtime) { + public void onTimeStarted(long elapsedRealtime, long baseUptime, long baseRealtime) { mUnpluggedCount = mPluggedCount; mCount = mPluggedCount; } @Override - public void plug(long elapsedRealtime, long batteryUptime, long batteryRealtime) { + public void onTimeStopped(long elapsedRealtime, long baseUptime, long baseRealtime) { mPluggedCount = mCount; } public long getCountLocked(int which) { - long val; - if (which == STATS_LAST) { - val = mLastCount; - } else { - val = mCount; - if (which == STATS_SINCE_UNPLUGGED) { - val -= mUnpluggedCount; - } else if (which != STATS_SINCE_CHARGED) { - val -= mLoadedCount; - } + long val = mCount; + if (which == STATS_SINCE_UNPLUGGED) { + val -= mUnpluggedCount; + } else if (which != STATS_SINCE_CHARGED) { + val -= mLoadedCount; } return val; } + @Override + public void logState(Printer pw, String prefix) { + pw.println(prefix + "mCount=" + mCount + + " mLoadedCount=" + mLoadedCount + " mLastCount=" + mLastCount + + " mUnpluggedCount=" + mUnpluggedCount + + " mPluggedCount=" + mPluggedCount); + } + void addCountLocked(long count) { mCount += count; } @@ -560,7 +807,7 @@ public final class BatteryStatsImpl extends BatteryStats { } void detach() { - mUnpluggables.remove(this); + mTimeBase.remove(this); } void writeSummaryFromParcelLocked(Parcel out) { @@ -578,9 +825,9 @@ public final class BatteryStatsImpl extends BatteryStats { /** * State for keeping track of timing information. */ - public static abstract class Timer extends BatteryStats.Timer implements Unpluggable { + public static abstract class Timer extends BatteryStats.Timer implements TimeBaseObs { final int mType; - final ArrayList<Unpluggable> mUnpluggables; + final TimeBase mTimeBase; int mCount; int mLoadedCount; @@ -619,12 +866,12 @@ public final class BatteryStatsImpl extends BatteryStats { /** * Constructs from a parcel. * @param type - * @param unpluggables + * @param timeBase * @param in */ - Timer(int type, ArrayList<Unpluggable> unpluggables, Parcel in) { + Timer(int type, TimeBase timeBase, Parcel in) { mType = type; - mUnpluggables = unpluggables; + mTimeBase = timeBase; mCount = in.readInt(); mLoadedCount = in.readInt(); @@ -634,13 +881,13 @@ public final class BatteryStatsImpl extends BatteryStats { mLoadedTime = in.readLong(); mLastTime = 0; mUnpluggedTime = in.readLong(); - unpluggables.add(this); + timeBase.add(this); } - Timer(int type, ArrayList<Unpluggable> unpluggables) { + Timer(int type, TimeBase timeBase) { mType = type; - mUnpluggables = unpluggables; - unpluggables.add(this); + mTimeBase = timeBase; + timeBase.add(this); } protected abstract long computeRunTimeLocked(long curBatteryRealtime); @@ -651,7 +898,7 @@ public final class BatteryStatsImpl extends BatteryStats { * Clear state of this timer. Returns true if the timer is inactive * so can be completely dropped. */ - boolean reset(BatteryStatsImpl stats, boolean detachIfReset) { + boolean reset(boolean detachIfReset) { mTotalTime = mLoadedTime = mLastTime = 0; mCount = mLoadedCount = mLastCount = 0; if (detachIfReset) { @@ -661,25 +908,25 @@ public final class BatteryStatsImpl extends BatteryStats { } void detach() { - mUnpluggables.remove(this); + mTimeBase.remove(this); } - public void writeToParcel(Parcel out, long batteryRealtime) { + public void writeToParcel(Parcel out, long elapsedRealtimeUs) { out.writeInt(mCount); out.writeInt(mLoadedCount); out.writeInt(mUnpluggedCount); - out.writeLong(computeRunTimeLocked(batteryRealtime)); + out.writeLong(computeRunTimeLocked(mTimeBase.getRealtime(elapsedRealtimeUs))); out.writeLong(mLoadedTime); out.writeLong(mUnpluggedTime); } - public void unplug(long elapsedRealtime, long batteryUptime, long batteryRealtime) { + public void onTimeStarted(long elapsedRealtime, long timeBaseUptime, long baseRealtime) { if (DEBUG && mType < 0) { - Log.v(TAG, "unplug #" + mType + ": realtime=" + batteryRealtime + Log.v(TAG, "unplug #" + mType + ": realtime=" + baseRealtime + " old mUnpluggedTime=" + mUnpluggedTime + " old mUnpluggedCount=" + mUnpluggedCount); } - mUnpluggedTime = computeRunTimeLocked(batteryRealtime); + mUnpluggedTime = computeRunTimeLocked(baseRealtime); mUnpluggedCount = mCount; if (DEBUG && mType < 0) { Log.v(TAG, "unplug #" + mType @@ -688,12 +935,12 @@ public final class BatteryStatsImpl extends BatteryStats { } } - public void plug(long elapsedRealtime, long batteryUptime, long batteryRealtime) { + public void onTimeStopped(long elapsedRealtime, long baseUptime, long baseRealtime) { if (DEBUG && mType < 0) { - Log.v(TAG, "plug #" + mType + ": realtime=" + batteryRealtime + Log.v(TAG, "plug #" + mType + ": realtime=" + baseRealtime + " old mTotalTime=" + mTotalTime); } - mTotalTime = computeRunTimeLocked(batteryRealtime); + mTotalTime = computeRunTimeLocked(baseRealtime); mCount = computeCurrentCountLocked(); if (DEBUG && mType < 0) { Log.v(TAG, "plug #" + mType @@ -707,29 +954,23 @@ public final class BatteryStatsImpl extends BatteryStats { * @param out the Parcel to be written to. * @param timer a Timer, or null. */ - public static void writeTimerToParcel(Parcel out, Timer timer, - long batteryRealtime) { + public static void writeTimerToParcel(Parcel out, Timer timer, long elapsedRealtimeUs) { if (timer == null) { out.writeInt(0); // indicates null return; } out.writeInt(1); // indicates non-null - timer.writeToParcel(out, batteryRealtime); + timer.writeToParcel(out, elapsedRealtimeUs); } @Override - public long getTotalTimeLocked(long batteryRealtime, int which) { - long val; - if (which == STATS_LAST) { - val = mLastTime; - } else { - val = computeRunTimeLocked(batteryRealtime); - if (which == STATS_SINCE_UNPLUGGED) { - val -= mUnpluggedTime; - } else if (which != STATS_SINCE_CHARGED) { - val -= mLoadedTime; - } + public long getTotalTimeLocked(long elapsedRealtimeUs, int which) { + long val = computeRunTimeLocked(mTimeBase.getRealtime(elapsedRealtimeUs)); + if (which == STATS_SINCE_UNPLUGGED) { + val -= mUnpluggedTime; + } else if (which != STATS_SINCE_CHARGED) { + val -= mLoadedTime; } return val; @@ -737,16 +978,11 @@ public final class BatteryStatsImpl extends BatteryStats { @Override public int getCountLocked(int which) { - int val; - if (which == STATS_LAST) { - val = mLastCount; - } else { - val = computeCurrentCountLocked(); - if (which == STATS_SINCE_UNPLUGGED) { - val -= mUnpluggedCount; - } else if (which != STATS_SINCE_CHARGED) { - val -= mLoadedCount; - } + int val = computeCurrentCountLocked(); + if (which == STATS_SINCE_UNPLUGGED) { + val -= mUnpluggedCount; + } else if (which != STATS_SINCE_CHARGED) { + val -= mLoadedCount; } return val; @@ -763,16 +999,15 @@ public final class BatteryStatsImpl extends BatteryStats { } - void writeSummaryFromParcelLocked(Parcel out, long batteryRealtime) { - long runTime = computeRunTimeLocked(batteryRealtime); - // Divide by 1000 for backwards compatibility - out.writeLong((runTime + 500) / 1000); + void writeSummaryFromParcelLocked(Parcel out, long elapsedRealtimeUs) { + long runTime = computeRunTimeLocked(mTimeBase.getRealtime(elapsedRealtimeUs)); + out.writeLong(runTime); out.writeInt(mCount); } void readSummaryFromParcelLocked(Parcel in) { // Multiply by 1000 for backwards compatibility - mTotalTime = mLoadedTime = in.readLong() * 1000; + mTotalTime = mLoadedTime = in.readLong(); mLastTime = 0; mUnpluggedTime = mTotalTime; mCount = mLoadedCount = in.readInt(); @@ -809,7 +1044,7 @@ public final class BatteryStatsImpl extends BatteryStats { /** * Whether we are currently in a discharge cycle. */ - boolean mInDischarge; + boolean mTimeBaseRunning; /** * Whether we are currently recording reported values. @@ -821,21 +1056,20 @@ public final class BatteryStatsImpl extends BatteryStats { */ int mUpdateVersion; - SamplingTimer(ArrayList<Unpluggable> unpluggables, boolean inDischarge, Parcel in) { - super(0, unpluggables, in); + SamplingTimer(TimeBase timeBase, Parcel in) { + super(0, timeBase, in); mCurrentReportedCount = in.readInt(); mUnpluggedReportedCount = in.readInt(); mCurrentReportedTotalTime = in.readLong(); mUnpluggedReportedTotalTime = in.readLong(); mTrackingReportedValues = in.readInt() == 1; - mInDischarge = inDischarge; + mTimeBaseRunning = timeBase.isRunning(); } - SamplingTimer(ArrayList<Unpluggable> unpluggables, boolean inDischarge, - boolean trackReportedValues) { - super(0, unpluggables); + SamplingTimer(TimeBase timeBase, boolean trackReportedValues) { + super(0, timeBase); mTrackingReportedValues = trackReportedValues; - mInDischarge = inDischarge; + mTimeBaseRunning = timeBase.isRunning(); } public void setStale() { @@ -853,7 +1087,7 @@ public final class BatteryStatsImpl extends BatteryStats { } public void updateCurrentReportedCount(int count) { - if (mInDischarge && mUnpluggedReportedCount == 0) { + if (mTimeBaseRunning && mUnpluggedReportedCount == 0) { // Updating the reported value for the first time. mUnpluggedReportedCount = count; // If we are receiving an update update mTrackingReportedValues; @@ -863,7 +1097,7 @@ public final class BatteryStatsImpl extends BatteryStats { } public void updateCurrentReportedTotalTime(long totalTime) { - if (mInDischarge && mUnpluggedReportedTotalTime == 0) { + if (mTimeBaseRunning && mUnpluggedReportedTotalTime == 0) { // Updating the reported value for the first time. mUnpluggedReportedTotalTime = totalTime; // If we are receiving an update update mTrackingReportedValues; @@ -872,18 +1106,18 @@ public final class BatteryStatsImpl extends BatteryStats { mCurrentReportedTotalTime = totalTime; } - public void unplug(long elapsedRealtime, long batteryUptime, long batteryRealtime) { - super.unplug(elapsedRealtime, batteryUptime, batteryRealtime); + public void onTimeStarted(long elapsedRealtime, long baseUptime, long baseRealtime) { + super.onTimeStarted(elapsedRealtime, baseUptime, baseRealtime); if (mTrackingReportedValues) { mUnpluggedReportedTotalTime = mCurrentReportedTotalTime; mUnpluggedReportedCount = mCurrentReportedCount; } - mInDischarge = true; + mTimeBaseRunning = true; } - public void plug(long elapsedRealtime, long batteryUptime, long batteryRealtime) { - super.plug(elapsedRealtime, batteryUptime, batteryRealtime); - mInDischarge = false; + public void onTimeStopped(long elapsedRealtime, long baseUptime, long baseRealtime) { + super.onTimeStopped(elapsedRealtime, baseUptime, baseRealtime); + mTimeBaseRunning = false; } public void logState(Printer pw, String prefix) { @@ -895,17 +1129,17 @@ public final class BatteryStatsImpl extends BatteryStats { } protected long computeRunTimeLocked(long curBatteryRealtime) { - return mTotalTime + (mInDischarge && mTrackingReportedValues + return mTotalTime + (mTimeBaseRunning && mTrackingReportedValues ? mCurrentReportedTotalTime - mUnpluggedReportedTotalTime : 0); } protected int computeCurrentCountLocked() { - return mCount + (mInDischarge && mTrackingReportedValues + return mCount + (mTimeBaseRunning && mTrackingReportedValues ? mCurrentReportedCount - mUnpluggedReportedCount : 0); } - public void writeToParcel(Parcel out, long batteryRealtime) { - super.writeToParcel(out, batteryRealtime); + public void writeToParcel(Parcel out, long elapsedRealtimeUs) { + super.writeToParcel(out, elapsedRealtimeUs); out.writeInt(mCurrentReportedCount); out.writeInt(mUnpluggedReportedCount); out.writeLong(mCurrentReportedTotalTime); @@ -913,8 +1147,8 @@ public final class BatteryStatsImpl extends BatteryStats { out.writeInt(mTrackingReportedValues ? 1 : 0); } - boolean reset(BatteryStatsImpl stats, boolean detachIfReset) { - super.reset(stats, detachIfReset); + boolean reset(boolean detachIfReset) { + super.reset(detachIfReset); setStale(); return true; } @@ -956,45 +1190,43 @@ public final class BatteryStatsImpl extends BatteryStats { */ boolean mInDischarge; - BatchTimer(Uid uid, int type, ArrayList<Unpluggable> unpluggables, - boolean inDischarge, Parcel in) { - super(type, unpluggables, in); + BatchTimer(Uid uid, int type, TimeBase timeBase, Parcel in) { + super(type, timeBase, in); mUid = uid; mLastAddedTime = in.readLong(); mLastAddedDuration = in.readLong(); - mInDischarge = inDischarge; + mInDischarge = timeBase.isRunning(); } - BatchTimer(Uid uid, int type, ArrayList<Unpluggable> unpluggables, - boolean inDischarge) { - super(type, unpluggables); + BatchTimer(Uid uid, int type, TimeBase timeBase) { + super(type, timeBase); mUid = uid; - mInDischarge = inDischarge; + mInDischarge = timeBase.isRunning(); } @Override - public void writeToParcel(Parcel out, long batteryRealtime) { - super.writeToParcel(out, batteryRealtime); + public void writeToParcel(Parcel out, long elapsedRealtimeUs) { + super.writeToParcel(out, elapsedRealtimeUs); out.writeLong(mLastAddedTime); out.writeLong(mLastAddedDuration); } @Override - public void plug(long elapsedRealtime, long batteryUptime, long batteryRealtime) { + public void onTimeStopped(long elapsedRealtime, long baseUptime, long baseRealtime) { recomputeLastDuration(SystemClock.elapsedRealtime() * 1000, false); mInDischarge = false; - super.plug(elapsedRealtime, batteryUptime, batteryRealtime); + super.onTimeStopped(elapsedRealtime, baseUptime, baseRealtime); } @Override - public void unplug(long elapsedRealtime, long batteryUptime, long batteryRealtime) { + public void onTimeStarted(long elapsedRealtime, long baseUptime, long baseRealtime) { recomputeLastDuration(elapsedRealtime, false); mInDischarge = true; // If we are still within the last added duration, then re-added whatever remains. if (mLastAddedTime == elapsedRealtime) { mTotalTime += mLastAddedDuration; } - super.unplug(elapsedRealtime, batteryUptime, batteryRealtime); + super.onTimeStarted(elapsedRealtime, baseUptime, baseRealtime); } @Override @@ -1060,11 +1292,11 @@ public final class BatteryStatsImpl extends BatteryStats { } @Override - boolean reset(BatteryStatsImpl stats, boolean detachIfReset) { + boolean reset(boolean detachIfReset) { final long now = SystemClock.elapsedRealtime() * 1000; recomputeLastDuration(now, true); boolean stillActive = mLastAddedTime == now; - super.reset(stats, !stillActive && detachIfReset); + super.reset(!stillActive && detachIfReset); return !stillActive; } } @@ -1100,16 +1332,16 @@ public final class BatteryStatsImpl extends BatteryStats { boolean mInList; StopwatchTimer(Uid uid, int type, ArrayList<StopwatchTimer> timerPool, - ArrayList<Unpluggable> unpluggables, Parcel in) { - super(type, unpluggables, in); + TimeBase timeBase, Parcel in) { + super(type, timeBase, in); mUid = uid; mTimerPool = timerPool; mUpdateTime = in.readLong(); } StopwatchTimer(Uid uid, int type, ArrayList<StopwatchTimer> timerPool, - ArrayList<Unpluggable> unpluggables) { - super(type, unpluggables); + TimeBase timeBase) { + super(type, timeBase); mUid = uid; mTimerPool = timerPool; } @@ -1118,18 +1350,18 @@ public final class BatteryStatsImpl extends BatteryStats { mTimeout = timeout; } - public void writeToParcel(Parcel out, long batteryRealtime) { - super.writeToParcel(out, batteryRealtime); + public void writeToParcel(Parcel out, long elapsedRealtimeUs) { + super.writeToParcel(out, elapsedRealtimeUs); out.writeLong(mUpdateTime); } - public void plug(long elapsedRealtime, long batteryUptime, long batteryRealtime) { + public void onTimeStopped(long elapsedRealtime, long baseUptime, long baseRealtime) { if (mNesting > 0) { if (DEBUG && mType < 0) { Log.v(TAG, "old mUpdateTime=" + mUpdateTime); } - super.plug(elapsedRealtime, batteryUptime, batteryRealtime); - mUpdateTime = batteryRealtime; + super.onTimeStopped(elapsedRealtime, baseUptime, baseRealtime); + mUpdateTime = baseRealtime; if (DEBUG && mType < 0) { Log.v(TAG, "new mUpdateTime=" + mUpdateTime); } @@ -1142,14 +1374,14 @@ public final class BatteryStatsImpl extends BatteryStats { + " mAcquireTime=" + mAcquireTime); } - void startRunningLocked(BatteryStatsImpl stats) { + void startRunningLocked(long elapsedRealtimeMs) { if (mNesting++ == 0) { - mUpdateTime = stats.getBatteryRealtimeLocked( - SystemClock.elapsedRealtime() * 1000); + final long batteryRealtime = mTimeBase.getRealtime(elapsedRealtimeMs * 1000); + mUpdateTime = batteryRealtime; if (mTimerPool != null) { // Accumulate time to all currently active timers before adding // this new one to the pool. - refreshTimersLocked(stats, mTimerPool); + refreshTimersLocked(batteryRealtime, mTimerPool, null); // Add this timer to the active pool mTimerPool.add(this); } @@ -1168,21 +1400,39 @@ public final class BatteryStatsImpl extends BatteryStats { return mNesting > 0; } - void stopRunningLocked(BatteryStatsImpl stats) { + long checkpointRunningLocked(long elapsedRealtimeMs) { + if (mNesting > 0) { + // We are running... + final long batteryRealtime = mTimeBase.getRealtime(elapsedRealtimeMs * 1000); + if (mTimerPool != null) { + return refreshTimersLocked(batteryRealtime, mTimerPool, this); + } + final long heldTime = batteryRealtime - mUpdateTime; + mUpdateTime = batteryRealtime; + mTotalTime += heldTime; + return heldTime; + } + return 0; + } + + long getLastUpdateTimeMs() { + return mUpdateTime; + } + + void stopRunningLocked(long elapsedRealtimeMs) { // Ignore attempt to stop a timer that isn't running if (mNesting == 0) { return; } if (--mNesting == 0) { + final long batteryRealtime = mTimeBase.getRealtime(elapsedRealtimeMs * 1000); if (mTimerPool != null) { // Accumulate time to all active counters, scaled by the total // active in the pool, before taking this one out of the pool. - refreshTimersLocked(stats, mTimerPool); + refreshTimersLocked(batteryRealtime, mTimerPool, null); // Remove this timer from the active pool mTimerPool.remove(this); } else { - final long realtime = SystemClock.elapsedRealtime() * 1000; - final long batteryRealtime = stats.getBatteryRealtimeLocked(realtime); mNesting = 1; mTotalTime = computeRunTimeLocked(batteryRealtime); mNesting = 0; @@ -1204,19 +1454,23 @@ public final class BatteryStatsImpl extends BatteryStats { // Update the total time for all other running Timers with the same type as this Timer // due to a change in timer count - private static void refreshTimersLocked(final BatteryStatsImpl stats, - final ArrayList<StopwatchTimer> pool) { - final long realtime = SystemClock.elapsedRealtime() * 1000; - final long batteryRealtime = stats.getBatteryRealtimeLocked(realtime); + private static long refreshTimersLocked(long batteryRealtime, + final ArrayList<StopwatchTimer> pool, StopwatchTimer self) { + long selfTime = 0; final int N = pool.size(); for (int i=N-1; i>= 0; i--) { final StopwatchTimer t = pool.get(i); long heldTime = batteryRealtime - t.mUpdateTime; if (heldTime > 0) { - t.mTotalTime += heldTime / N; + final long myTime = heldTime / N; + if (t == self) { + selfTime = myTime; + } + t.mTotalTime += myTime; } t.mUpdateTime = batteryRealtime; } + return selfTime; } @Override @@ -1235,12 +1489,11 @@ public final class BatteryStatsImpl extends BatteryStats { return mCount; } - boolean reset(BatteryStatsImpl stats, boolean detachIfReset) { + boolean reset(boolean detachIfReset) { boolean canDetach = mNesting <= 0; - super.reset(stats, canDetach && detachIfReset); + super.reset(canDetach && detachIfReset); if (mNesting > 0) { - mUpdateTime = stats.getBatteryRealtimeLocked( - SystemClock.elapsedRealtime() * 1000); + mUpdateTime = mTimeBase.getRealtime(SystemClock.elapsedRealtime() * 1000); } mAcquireTime = mTotalTime; return canDetach; @@ -1259,6 +1512,19 @@ public final class BatteryStatsImpl extends BatteryStats { } } + /* + * Get the wakeup reason counter, and create a new one if one + * doesn't already exist. + */ + public LongSamplingCounter getWakeupReasonCounterLocked(String name) { + LongSamplingCounter counter = mWakeupReasonStats.get(name); + if (counter == null) { + counter = new LongSamplingCounter(mOnBatteryScreenOffTimeBase); + mWakeupReasonStats.put(name, counter); + } + return counter; + } + private final Map<String, KernelWakelockStats> readKernelWakelockStats() { FileInputStream is; @@ -1403,51 +1669,12 @@ public final class BatteryStatsImpl extends BatteryStats { public SamplingTimer getKernelWakelockTimerLocked(String name) { SamplingTimer kwlt = mKernelWakelockStats.get(name); if (kwlt == null) { - kwlt = new SamplingTimer(mUnpluggables, mOnBatteryInternal, - true /* track reported values */); + kwlt = new SamplingTimer(mOnBatteryScreenOffTimeBase, true /* track reported values */); mKernelWakelockStats.put(name, kwlt); } return kwlt; } - /** - * Radio uptime in microseconds when transferring data. This value is very approximate. - * @return - */ - private long getCurrentRadioDataUptime() { - try { - File awakeTimeFile = new File("/sys/devices/virtual/net/rmnet0/awake_time_ms"); - if (!awakeTimeFile.exists()) return 0; - BufferedReader br = new BufferedReader(new FileReader(awakeTimeFile)); - String line = br.readLine(); - br.close(); - return Long.parseLong(line) * 1000; - } catch (NumberFormatException nfe) { - // Nothing - } catch (IOException ioe) { - // Nothing - } - return 0; - } - - /** - * @deprecated use getRadioDataUptime - */ - public long getRadioDataUptimeMs() { - return getRadioDataUptime() / 1000; - } - - /** - * Returns the duration that the cell radio was up for data transfers. - */ - public long getRadioDataUptime() { - if (mRadioDataStart == -1) { - return mRadioDataUptime; - } else { - return getCurrentRadioDataUptime() - mRadioDataStart; - } - } - private int getCurrentBluetoothPingCount() { if (mBtHeadset != null) { List<BluetoothDevice> deviceList = mBtHeadset.getConnectedDevices(); @@ -1474,83 +1701,434 @@ public final class BatteryStatsImpl extends BatteryStats { mBtHeadset = headset; } - int mChangedBufferStates = 0; + private int writeHistoryTag(HistoryTag tag) { + Integer idxObj = mHistoryTagPool.get(tag); + int idx; + if (idxObj != null) { + idx = idxObj; + } else { + idx = mNextHistoryTagIdx; + HistoryTag key = new HistoryTag(); + key.setTo(tag); + tag.poolIdx = idx; + mHistoryTagPool.put(key, idx); + mNextHistoryTagIdx++; + mNumHistoryTagChars += key.string.length() + 1; + } + return idx; + } + + private void readHistoryTag(int index, HistoryTag tag) { + tag.string = mReadHistoryStrings[index]; + tag.uid = mReadHistoryUids[index]; + tag.poolIdx = index; + } + + // Part of initial delta int that specifies the time delta. + static final int DELTA_TIME_MASK = 0x7ffff; + static final int DELTA_TIME_LONG = 0x7ffff; // The delta is a following long + static final int DELTA_TIME_INT = 0x7fffe; // The delta is a following int + static final int DELTA_TIME_ABS = 0x7fffd; // Following is an entire abs update. + // Flag in delta int: a new battery level int follows. + static final int DELTA_BATTERY_LEVEL_FLAG = 0x00080000; + // Flag in delta int: a new full state and battery status int follows. + static final int DELTA_STATE_FLAG = 0x00100000; + // Flag in delta int: a new full state2 int follows. + static final int DELTA_STATE2_FLAG = 0x00200000; + // Flag in delta int: contains a wakelock or wakeReason tag. + static final int DELTA_WAKELOCK_FLAG = 0x00400000; + // Flag in delta int: contains an event description. + static final int DELTA_EVENT_FLAG = 0x00800000; + // These upper bits are the frequently changing state bits. + static final int DELTA_STATE_MASK = 0xff000000; + + // These are the pieces of battery state that are packed in to the upper bits of + // the state int that have been packed in to the first delta int. They must fit + // in DELTA_STATE_MASK. + static final int STATE_BATTERY_STATUS_MASK = 0x00000007; + static final int STATE_BATTERY_STATUS_SHIFT = 29; + static final int STATE_BATTERY_HEALTH_MASK = 0x00000007; + static final int STATE_BATTERY_HEALTH_SHIFT = 26; + static final int STATE_BATTERY_PLUG_MASK = 0x00000003; + static final int STATE_BATTERY_PLUG_SHIFT = 24; + + public void writeHistoryDelta(Parcel dest, HistoryItem cur, HistoryItem last) { + if (last == null || cur.cmd != HistoryItem.CMD_UPDATE) { + dest.writeInt(DELTA_TIME_ABS); + cur.writeToParcel(dest, 0); + return; + } + + final long deltaTime = cur.time - last.time; + final int lastBatteryLevelInt = buildBatteryLevelInt(last); + final int lastStateInt = buildStateInt(last); + + int deltaTimeToken; + if (deltaTime < 0 || deltaTime > Integer.MAX_VALUE) { + deltaTimeToken = DELTA_TIME_LONG; + } else if (deltaTime >= DELTA_TIME_ABS) { + deltaTimeToken = DELTA_TIME_INT; + } else { + deltaTimeToken = (int)deltaTime; + } + int firstToken = deltaTimeToken | (cur.states&DELTA_STATE_MASK); + final int batteryLevelInt = buildBatteryLevelInt(cur); + final boolean batteryLevelIntChanged = batteryLevelInt != lastBatteryLevelInt; + if (batteryLevelIntChanged) { + firstToken |= DELTA_BATTERY_LEVEL_FLAG; + } + final int stateInt = buildStateInt(cur); + final boolean stateIntChanged = stateInt != lastStateInt; + if (stateIntChanged) { + firstToken |= DELTA_STATE_FLAG; + } + final boolean state2IntChanged = cur.states2 != last.states2; + if (state2IntChanged) { + firstToken |= DELTA_STATE2_FLAG; + } + if (cur.wakelockTag != null || cur.wakeReasonTag != null) { + firstToken |= DELTA_WAKELOCK_FLAG; + } + if (cur.eventCode != HistoryItem.EVENT_NONE) { + firstToken |= DELTA_EVENT_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=" + cur.batteryLevel + + " batteryTemp=" + cur.batteryTemperature + + " batteryVolt=" + (int)cur.batteryVoltage); + } + if (stateIntChanged) { + dest.writeInt(stateInt); + if (DEBUG) Slog.i(TAG, "WRITE DELTA: stateToken=0x" + + Integer.toHexString(stateInt) + + " batteryStatus=" + cur.batteryStatus + + " batteryHealth=" + cur.batteryHealth + + " batteryPlugType=" + cur.batteryPlugType + + " states=0x" + Integer.toHexString(cur.states)); + } + if (state2IntChanged) { + dest.writeInt(cur.states2); + if (DEBUG) Slog.i(TAG, "WRITE DELTA: states2=0x" + + Integer.toHexString(cur.states2)); + } + if (cur.wakelockTag != null || cur.wakeReasonTag != null) { + int wakeLockIndex; + int wakeReasonIndex; + if (cur.wakelockTag != null) { + wakeLockIndex = writeHistoryTag(cur.wakelockTag); + if (DEBUG) Slog.i(TAG, "WRITE DELTA: wakelockTag=#" + cur.wakelockTag.poolIdx + + " " + cur.wakelockTag.uid + ":" + cur.wakelockTag.string); + } else { + wakeLockIndex = 0xffff; + } + if (cur.wakeReasonTag != null) { + wakeReasonIndex = writeHistoryTag(cur.wakeReasonTag); + if (DEBUG) Slog.i(TAG, "WRITE DELTA: wakeReasonTag=#" + cur.wakeReasonTag.poolIdx + + " " + cur.wakeReasonTag.uid + ":" + cur.wakeReasonTag.string); + } else { + wakeReasonIndex = 0xffff; + } + dest.writeInt((wakeReasonIndex<<16) | wakeLockIndex); + } + if (cur.eventCode != HistoryItem.EVENT_NONE) { + int index = writeHistoryTag(cur.eventTag); + int codeAndIndex = (cur.eventCode&0xffff) | (index<<16); + dest.writeInt(codeAndIndex); + if (DEBUG) Slog.i(TAG, "WRITE DELTA: event=" + cur.eventCode + " tag=#" + + cur.eventTag.poolIdx + " " + cur.eventTag.uid + ":" + + cur.eventTag.string); + } + } + + private int buildBatteryLevelInt(HistoryItem h) { + return ((((int)h.batteryLevel)<<25)&0xfe000000) + | ((((int)h.batteryTemperature)<<14)&0x01ffc000) + | (((int)h.batteryVoltage)&0x00003fff); + } + + private int buildStateInt(HistoryItem h) { + int plugType = 0; + if ((h.batteryPlugType&BatteryManager.BATTERY_PLUGGED_AC) != 0) { + plugType = 1; + } else if ((h.batteryPlugType&BatteryManager.BATTERY_PLUGGED_USB) != 0) { + plugType = 2; + } else if ((h.batteryPlugType&BatteryManager.BATTERY_PLUGGED_WIRELESS) != 0) { + plugType = 3; + } + return ((h.batteryStatus&STATE_BATTERY_STATUS_MASK)<<STATE_BATTERY_STATUS_SHIFT) + | ((h.batteryHealth&STATE_BATTERY_HEALTH_MASK)<<STATE_BATTERY_HEALTH_SHIFT) + | ((plugType&STATE_BATTERY_PLUG_MASK)<<STATE_BATTERY_PLUG_SHIFT) + | (h.states&(~DELTA_STATE_MASK)); + } + + public void readHistoryDelta(Parcel src, HistoryItem cur) { + int firstToken = src.readInt(); + int deltaTimeToken = firstToken&DELTA_TIME_MASK; + cur.cmd = HistoryItem.CMD_UPDATE; + cur.numReadInts = 1; + if (DEBUG) Slog.i(TAG, "READ DELTA: firstToken=0x" + Integer.toHexString(firstToken) + + " deltaTimeToken=" + deltaTimeToken); + + if (deltaTimeToken < DELTA_TIME_ABS) { + cur.time += deltaTimeToken; + } else if (deltaTimeToken == DELTA_TIME_ABS) { + cur.time = src.readLong(); + cur.numReadInts += 2; + if (DEBUG) Slog.i(TAG, "READ DELTA: ABS time=" + cur.time); + cur.readFromParcel(src); + return; + } else if (deltaTimeToken == DELTA_TIME_INT) { + int delta = src.readInt(); + cur.time += delta; + cur.numReadInts += 1; + if (DEBUG) Slog.i(TAG, "READ DELTA: time delta=" + delta + " new time=" + cur.time); + } else { + long delta = src.readLong(); + if (DEBUG) Slog.i(TAG, "READ DELTA: time delta=" + delta + " new time=" + cur.time); + cur.time += delta; + cur.numReadInts += 2; + } + + if ((firstToken&DELTA_BATTERY_LEVEL_FLAG) != 0) { + int batteryLevelInt = src.readInt(); + cur.batteryLevel = (byte)((batteryLevelInt>>25)&0x7f); + cur.batteryTemperature = (short)((batteryLevelInt<<7)>>21); + cur.batteryVoltage = (char)(batteryLevelInt&0x3fff); + cur.numReadInts += 1; + if (DEBUG) Slog.i(TAG, "READ DELTA: batteryToken=0x" + + Integer.toHexString(batteryLevelInt) + + " batteryLevel=" + cur.batteryLevel + + " batteryTemp=" + cur.batteryTemperature + + " batteryVolt=" + (int)cur.batteryVoltage); + } + + if ((firstToken&DELTA_STATE_FLAG) != 0) { + int stateInt = src.readInt(); + cur.states = (firstToken&DELTA_STATE_MASK) | (stateInt&(~DELTA_STATE_MASK)); + cur.batteryStatus = (byte)((stateInt>>STATE_BATTERY_STATUS_SHIFT) + & STATE_BATTERY_STATUS_MASK); + cur.batteryHealth = (byte)((stateInt>>STATE_BATTERY_HEALTH_SHIFT) + & STATE_BATTERY_HEALTH_MASK); + cur.batteryPlugType = (byte)((stateInt>>STATE_BATTERY_PLUG_SHIFT) + & STATE_BATTERY_PLUG_MASK); + switch (cur.batteryPlugType) { + case 1: + cur.batteryPlugType = BatteryManager.BATTERY_PLUGGED_AC; + break; + case 2: + cur.batteryPlugType = BatteryManager.BATTERY_PLUGGED_USB; + break; + case 3: + cur.batteryPlugType = BatteryManager.BATTERY_PLUGGED_WIRELESS; + break; + } + cur.numReadInts += 1; + if (DEBUG) Slog.i(TAG, "READ DELTA: stateToken=0x" + + Integer.toHexString(stateInt) + + " batteryStatus=" + cur.batteryStatus + + " batteryHealth=" + cur.batteryHealth + + " batteryPlugType=" + cur.batteryPlugType + + " states=0x" + Integer.toHexString(cur.states)); + } else { + cur.states = (firstToken&DELTA_STATE_MASK) | (cur.states&(~DELTA_STATE_MASK)); + } + + if ((firstToken&DELTA_STATE2_FLAG) != 0) { + cur.states2 = src.readInt(); + if (DEBUG) Slog.i(TAG, "READ DELTA: states2=0x" + + Integer.toHexString(cur.states2)); + } + + if ((firstToken&DELTA_WAKELOCK_FLAG) != 0) { + int indexes = src.readInt(); + int wakeLockIndex = indexes&0xffff; + int wakeReasonIndex = (indexes>>16)&0xffff; + if (wakeLockIndex != 0xffff) { + cur.wakelockTag = cur.localWakelockTag; + readHistoryTag(wakeLockIndex, cur.wakelockTag); + if (DEBUG) Slog.i(TAG, "READ DELTA: wakelockTag=#" + cur.wakelockTag.poolIdx + + " " + cur.wakelockTag.uid + ":" + cur.wakelockTag.string); + } else { + cur.wakelockTag = null; + } + if (wakeReasonIndex != 0xffff) { + cur.wakeReasonTag = cur.localWakeReasonTag; + readHistoryTag(wakeReasonIndex, cur.wakeReasonTag); + if (DEBUG) Slog.i(TAG, "READ DELTA: wakeReasonTag=#" + cur.wakeReasonTag.poolIdx + + " " + cur.wakeReasonTag.uid + ":" + cur.wakeReasonTag.string); + } else { + cur.wakeReasonTag = null; + } + cur.numReadInts += 1; + } else { + cur.wakelockTag = null; + cur.wakeReasonTag = null; + } + + if ((firstToken&DELTA_EVENT_FLAG) != 0) { + cur.eventTag = cur.localEventTag; + final int codeAndIndex = src.readInt(); + cur.eventCode = (codeAndIndex&0xffff); + final int index = ((codeAndIndex>>16)&0xffff); + readHistoryTag(index, cur.eventTag); + cur.numReadInts += 1; + if (DEBUG) Slog.i(TAG, "READ DELTA: event=" + cur.eventCode + " tag=#" + + cur.eventTag.poolIdx + " " + cur.eventTag.uid + ":" + + cur.eventTag.string); + } else { + cur.eventCode = HistoryItem.EVENT_NONE; + } + } - void addHistoryBufferLocked(long curTime) { + void addHistoryBufferLocked(long elapsedRealtimeMs, long uptimeMs, HistoryItem cur) { if (!mHaveBatteryLevel || !mRecordingHistory) { return; } - final long timeDiff = (mHistoryBaseTime+curTime) - mHistoryLastWritten.time; + final long timeDiff = (mHistoryBaseTime+elapsedRealtimeMs) - mHistoryLastWritten.time; + final int diffStates = mHistoryLastWritten.states^cur.states; + final int diffStates2 = mHistoryLastWritten.states2^cur.states2; + final int lastDiffStates = mHistoryLastWritten.states^mHistoryLastLastWritten.states; + final int lastDiffStates2 = mHistoryLastWritten.states2^mHistoryLastLastWritten.states2; + if (DEBUG) Slog.i(TAG, "ADD: tdelta=" + timeDiff + " diff=" + + Integer.toHexString(diffStates) + " lastDiff=" + + Integer.toHexString(lastDiffStates) + " diff2=" + + Integer.toHexString(diffStates2) + " lastDiff2=" + + Integer.toHexString(lastDiffStates2)); if (mHistoryBufferLastPos >= 0 && mHistoryLastWritten.cmd == HistoryItem.CMD_UPDATE - && timeDiff < 2000 - && ((mHistoryLastWritten.states^mHistoryCur.states)&mChangedBufferStates) == 0) { - // If the current is the same as the one before, then we no - // longer need the entry. + && timeDiff < 1000 && (diffStates&lastDiffStates) == 0 + && (diffStates2&lastDiffStates2) == 0 + && (mHistoryLastWritten.wakelockTag == null || cur.wakelockTag == null) + && (mHistoryLastWritten.wakeReasonTag == null || cur.wakeReasonTag == null) + && (mHistoryLastWritten.eventCode == HistoryItem.EVENT_NONE + || cur.eventCode == HistoryItem.EVENT_NONE) + && mHistoryLastWritten.batteryLevel == cur.batteryLevel + && mHistoryLastWritten.batteryStatus == cur.batteryStatus + && mHistoryLastWritten.batteryHealth == cur.batteryHealth + && mHistoryLastWritten.batteryPlugType == cur.batteryPlugType + && mHistoryLastWritten.batteryTemperature == cur.batteryTemperature + && mHistoryLastWritten.batteryVoltage == cur.batteryVoltage) { + // We can merge this new change in with the last one. Merging is + // allowed as long as only the states have changed, and within those states + // as long as no bit has changed both between now and the last entry, as + // well as the last entry and the one before it (so we capture any toggles). + if (DEBUG) Slog.i(TAG, "ADD: rewinding back to " + mHistoryBufferLastPos); mHistoryBuffer.setDataSize(mHistoryBufferLastPos); mHistoryBuffer.setDataPosition(mHistoryBufferLastPos); mHistoryBufferLastPos = -1; - if (mHistoryLastLastWritten.cmd == HistoryItem.CMD_UPDATE - && timeDiff < 500 && mHistoryLastLastWritten.same(mHistoryCur)) { - // If this results in us returning to the state written - // prior to the last one, then we can just delete the last - // written one and drop the new one. Nothing more to do. - mHistoryLastWritten.setTo(mHistoryLastLastWritten); - mHistoryLastLastWritten.cmd = HistoryItem.CMD_NULL; - return; + elapsedRealtimeMs = mHistoryLastWritten.time - mHistoryBaseTime; + // If the last written history had a wakelock tag, we need to retain it. + // Note that the condition above made sure that we aren't in a case where + // both it and the current history item have a wakelock tag. + if (mHistoryLastWritten.wakelockTag != null) { + cur.wakelockTag = cur.localWakelockTag; + cur.wakelockTag.setTo(mHistoryLastWritten.wakelockTag); + } + // If the last written history had a wake reason tag, we need to retain it. + // Note that the condition above made sure that we aren't in a case where + // both it and the current history item have a wakelock tag. + if (mHistoryLastWritten.wakeReasonTag != null) { + cur.wakeReasonTag = cur.localWakeReasonTag; + cur.wakeReasonTag.setTo(mHistoryLastWritten.wakeReasonTag); + } + // If the last written history had an event, we need to retain it. + // Note that the condition above made sure that we aren't in a case where + // both it and the current history item have an event. + if (mHistoryLastWritten.eventCode != HistoryItem.EVENT_NONE) { + cur.eventCode = mHistoryLastWritten.eventCode; + cur.eventTag = cur.localEventTag; + cur.eventTag.setTo(mHistoryLastWritten.eventTag); } - mChangedBufferStates |= mHistoryLastWritten.states^mHistoryCur.states; - curTime = mHistoryLastWritten.time - mHistoryBaseTime; mHistoryLastWritten.setTo(mHistoryLastLastWritten); - } else { - mChangedBufferStates = 0; } final int dataSize = mHistoryBuffer.dataSize(); if (dataSize >= MAX_HISTORY_BUFFER) { if (!mHistoryOverflow) { mHistoryOverflow = true; - addHistoryBufferLocked(curTime, HistoryItem.CMD_OVERFLOW); + addHistoryBufferLocked(elapsedRealtimeMs, uptimeMs, HistoryItem.CMD_UPDATE, cur); + addHistoryBufferLocked(elapsedRealtimeMs, uptimeMs, HistoryItem.CMD_OVERFLOW, cur); + return; } // Once we've reached the maximum number of items, we only // record changes to the battery level and the most interesting states. // Once we've reached the maximum maximum number of items, we only // record changes to the battery level. - if (mHistoryLastWritten.batteryLevel == mHistoryCur.batteryLevel && + if (mHistoryLastWritten.batteryLevel == cur.batteryLevel && (dataSize >= MAX_MAX_HISTORY_BUFFER - || ((mHistoryLastWritten.states^mHistoryCur.states) + || ((mHistoryLastWritten.states^cur.states) & HistoryItem.MOST_INTERESTING_STATES) == 0)) { return; } + + addHistoryBufferLocked(elapsedRealtimeMs, uptimeMs, HistoryItem.CMD_UPDATE, cur); + return; } - addHistoryBufferLocked(curTime, HistoryItem.CMD_UPDATE); + addHistoryBufferLocked(elapsedRealtimeMs, uptimeMs, HistoryItem.CMD_UPDATE, cur); } - void addHistoryBufferLocked(long curTime, byte cmd) { - int origPos = 0; + private void addHistoryBufferLocked(long elapsedRealtimeMs, long uptimeMs, byte cmd, + HistoryItem cur) { if (mIteratingHistory) { - origPos = mHistoryBuffer.dataPosition(); - mHistoryBuffer.setDataPosition(mHistoryBuffer.dataSize()); + throw new IllegalStateException("Can't do this while iterating history!"); } mHistoryBufferLastPos = mHistoryBuffer.dataPosition(); mHistoryLastLastWritten.setTo(mHistoryLastWritten); - mHistoryLastWritten.setTo(mHistoryBaseTime + curTime, cmd, mHistoryCur); - mHistoryLastWritten.writeDelta(mHistoryBuffer, mHistoryLastLastWritten); - mLastHistoryTime = curTime; + mHistoryLastWritten.setTo(mHistoryBaseTime + elapsedRealtimeMs, cmd, cur); + writeHistoryDelta(mHistoryBuffer, mHistoryLastWritten, mHistoryLastLastWritten); + mLastHistoryElapsedRealtime = elapsedRealtimeMs; + cur.wakelockTag = null; + cur.wakeReasonTag = null; + cur.eventCode = HistoryItem.EVENT_NONE; + cur.eventTag = null; if (DEBUG_HISTORY) Slog.i(TAG, "Writing history buffer: was " + mHistoryBufferLastPos + " now " + mHistoryBuffer.dataPosition() + " size is now " + mHistoryBuffer.dataSize()); - if (mIteratingHistory) { - mHistoryBuffer.setDataPosition(origPos); - } } int mChangedStates = 0; + int mChangedStates2 = 0; - void addHistoryRecordLocked(long curTime) { - addHistoryBufferLocked(curTime); + void addHistoryRecordLocked(long elapsedRealtimeMs, long uptimeMs) { + if (mTrackRunningHistoryElapsedRealtime != 0) { + final long diffElapsed = elapsedRealtimeMs - mTrackRunningHistoryElapsedRealtime; + final long diffUptime = uptimeMs - mTrackRunningHistoryUptime; + if (diffUptime < (diffElapsed-20)) { + final long wakeElapsedTime = elapsedRealtimeMs - (diffElapsed - diffUptime); + mHistoryAddTmp.setTo(mHistoryLastWritten); + mHistoryAddTmp.wakelockTag = null; + mHistoryAddTmp.wakeReasonTag = null; + mHistoryAddTmp.eventCode = HistoryItem.EVENT_NONE; + mHistoryAddTmp.states &= ~HistoryItem.STATE_CPU_RUNNING_FLAG; + addHistoryRecordInnerLocked(wakeElapsedTime, uptimeMs, mHistoryAddTmp); + } + } + mHistoryCur.states |= HistoryItem.STATE_CPU_RUNNING_FLAG; + mTrackRunningHistoryElapsedRealtime = elapsedRealtimeMs; + mTrackRunningHistoryUptime = uptimeMs; + addHistoryRecordInnerLocked(elapsedRealtimeMs, uptimeMs, mHistoryCur); + } + + void addHistoryRecordInnerLocked(long elapsedRealtimeMs, long uptimeMs, HistoryItem cur) { + addHistoryBufferLocked(elapsedRealtimeMs, uptimeMs, cur); if (!USE_OLD_HISTORY) { return; @@ -1565,30 +2143,33 @@ public final class BatteryStatsImpl extends BatteryStats { // are now resetting back to their original value, then just collapse // into one record. if (mHistoryEnd != null && mHistoryEnd.cmd == HistoryItem.CMD_UPDATE - && (mHistoryBaseTime+curTime) < (mHistoryEnd.time+2000) - && ((mHistoryEnd.states^mHistoryCur.states)&mChangedStates) == 0) { + && (mHistoryBaseTime+elapsedRealtimeMs) < (mHistoryEnd.time+1000) + && ((mHistoryEnd.states^cur.states)&mChangedStates) == 0 + && ((mHistoryEnd.states2^cur.states2)&mChangedStates2) == 0) { // If the current is the same as the one before, then we no // longer need the entry. if (mHistoryLastEnd != null && mHistoryLastEnd.cmd == HistoryItem.CMD_UPDATE - && (mHistoryBaseTime+curTime) < (mHistoryEnd.time+500) - && mHistoryLastEnd.same(mHistoryCur)) { + && (mHistoryBaseTime+elapsedRealtimeMs) < (mHistoryEnd.time+500) + && mHistoryLastEnd.sameNonEvent(cur)) { mHistoryLastEnd.next = null; mHistoryEnd.next = mHistoryCache; mHistoryCache = mHistoryEnd; mHistoryEnd = mHistoryLastEnd; mHistoryLastEnd = null; } else { - mChangedStates |= mHistoryEnd.states^mHistoryCur.states; - mHistoryEnd.setTo(mHistoryEnd.time, HistoryItem.CMD_UPDATE, mHistoryCur); + mChangedStates |= mHistoryEnd.states^cur.states; + mChangedStates2 |= mHistoryEnd.states^cur.states2; + mHistoryEnd.setTo(mHistoryEnd.time, HistoryItem.CMD_UPDATE, cur); } return; } mChangedStates = 0; + mChangedStates2 = 0; if (mNumHistoryItems == MAX_HISTORY_ITEMS || mNumHistoryItems == MAX_MAX_HISTORY_ITEMS) { - addHistoryRecordLocked(curTime, HistoryItem.CMD_OVERFLOW); + addHistoryRecordLocked(elapsedRealtimeMs, HistoryItem.CMD_OVERFLOW); } if (mNumHistoryItems >= MAX_HISTORY_ITEMS) { @@ -1597,25 +2178,34 @@ public final class BatteryStatsImpl extends BatteryStats { // Once we've reached the maximum maximum number of items, we only // record changes to the battery level. if (mHistoryEnd != null && mHistoryEnd.batteryLevel - == mHistoryCur.batteryLevel && + == cur.batteryLevel && (mNumHistoryItems >= MAX_MAX_HISTORY_ITEMS - || ((mHistoryEnd.states^mHistoryCur.states) + || ((mHistoryEnd.states^cur.states) & HistoryItem.MOST_INTERESTING_STATES) == 0)) { return; } } - addHistoryRecordLocked(curTime, HistoryItem.CMD_UPDATE); + addHistoryRecordLocked(elapsedRealtimeMs, HistoryItem.CMD_UPDATE); } - void addHistoryRecordLocked(long curTime, byte cmd) { + void addHistoryEventLocked(long elapsedRealtimeMs, long uptimeMs, int code, + String name, int uid) { + mHistoryCur.eventCode = code; + mHistoryCur.eventTag = mHistoryCur.localEventTag; + mHistoryCur.eventTag.string = name; + mHistoryCur.eventTag.uid = uid; + addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs); + } + + void addHistoryRecordLocked(long elapsedRealtimeMs, long uptimeMs, byte cmd, HistoryItem cur) { HistoryItem rec = mHistoryCache; if (rec != null) { mHistoryCache = rec.next; } else { rec = new HistoryItem(); } - rec.setTo(mHistoryBaseTime + curTime, cmd, mHistoryCur); + rec.setTo(mHistoryBaseTime + elapsedRealtimeMs, cmd, cur); addHistoryRecordLocked(rec); } @@ -1644,114 +2234,252 @@ public final class BatteryStatsImpl extends BatteryStats { } mHistoryBaseTime = 0; - mLastHistoryTime = 0; + mLastHistoryElapsedRealtime = 0; + mTrackRunningHistoryElapsedRealtime = 0; + mTrackRunningHistoryUptime = 0; mHistoryBuffer.setDataSize(0); mHistoryBuffer.setDataPosition(0); - mHistoryBuffer.setDataCapacity(MAX_HISTORY_BUFFER/2); - mHistoryLastLastWritten.cmd = HistoryItem.CMD_NULL; - mHistoryLastWritten.cmd = HistoryItem.CMD_NULL; + mHistoryBuffer.setDataCapacity(MAX_HISTORY_BUFFER / 2); + mHistoryLastLastWritten.clear(); + mHistoryLastWritten.clear(); + mHistoryTagPool.clear(); + mNextHistoryTagIdx = 0; + mNumHistoryTagChars = 0; mHistoryBufferLastPos = -1; mHistoryOverflow = false; } - public void doUnplugLocked(long elapsedRealtime, long batteryUptime, long batteryRealtime) { - for (int i = mUnpluggables.size() - 1; i >= 0; i--) { - mUnpluggables.get(i).unplug(elapsedRealtime, batteryUptime, batteryRealtime); + public void updateTimeBasesLocked(boolean unplugged, boolean screenOff, long uptime, + long realtime) { + if (mOnBatteryTimeBase.setRunning(unplugged, uptime, realtime)) { + if (unplugged) { + // Track bt headset ping count + mBluetoothPingStart = getCurrentBluetoothPingCount(); + mBluetoothPingCount = 0; + } else { + // Track bt headset ping count + mBluetoothPingCount = getBluetoothPingCount(); + mBluetoothPingStart = -1; + } } - // Track radio awake time - mRadioDataStart = getCurrentRadioDataUptime(); - mRadioDataUptime = 0; + boolean unpluggedScreenOff = unplugged && screenOff; + if (unpluggedScreenOff != mOnBatteryScreenOffTimeBase.isRunning()) { + updateKernelWakelocksLocked(); + requestWakelockCpuUpdate(); + if (!unpluggedScreenOff) { + // We are switching to no longer tracking wake locks, but we want + // the next CPU update we receive to take them in to account. + mDistributeWakelockCpu = true; + } + mOnBatteryScreenOffTimeBase.setRunning(unpluggedScreenOff, uptime, realtime); + } + } - // Track bt headset ping count - mBluetoothPingStart = getCurrentBluetoothPingCount(); - mBluetoothPingCount = 0; + public void addIsolatedUidLocked(int isolatedUid, int appUid) { + mIsolatedUids.put(isolatedUid, appUid); } - public void doPlugLocked(long elapsedRealtime, long batteryUptime, long batteryRealtime) { - for (int i = mUnpluggables.size() - 1; i >= 0; i--) { - mUnpluggables.get(i).plug(elapsedRealtime, batteryUptime, batteryRealtime); + public void removeIsolatedUidLocked(int isolatedUid, int appUid) { + int curUid = mIsolatedUids.get(isolatedUid, -1); + if (curUid == appUid) { + mIsolatedUids.delete(isolatedUid); } + } - // Track radio awake time - mRadioDataUptime = getRadioDataUptime(); - mRadioDataStart = -1; + public int mapUid(int uid) { + int isolated = mIsolatedUids.get(uid, -1); + return isolated > 0 ? isolated : uid; + } - // Track bt headset ping count - mBluetoothPingCount = getBluetoothPingCount(); - mBluetoothPingStart = -1; + public void noteEventLocked(int code, String name, int uid) { + uid = mapUid(uid); + if ((code&HistoryItem.EVENT_FLAG_START) != 0) { + int idx = code&~HistoryItem.EVENT_FLAG_START; + HashMap<String, SparseBooleanArray> active = mActiveEvents[idx]; + if (active == null) { + active = new HashMap<String, SparseBooleanArray>(); + mActiveEvents[idx] = active; + } + SparseBooleanArray uids = active.get(name); + if (uids == null) { + uids = new SparseBooleanArray(); + active.put(name, uids); + } + if (uids.get(uid)) { + // Already set, nothing to do! + return; + } + uids.put(uid, true); + } else if ((code&HistoryItem.EVENT_FLAG_FINISH) != 0) { + int idx = code&~HistoryItem.EVENT_FLAG_FINISH; + HashMap<String, SparseBooleanArray> active = mActiveEvents[idx]; + if (active == null) { + // not currently active, nothing to do. + return; + } + SparseBooleanArray uids = active.get(name); + if (uids == null) { + // not currently active, nothing to do. + return; + } + idx = uids.indexOfKey(uid); + if (idx < 0 || !uids.valueAt(idx)) { + // not currently active, nothing to do. + return; + } + uids.removeAt(idx); + if (uids.size() <= 0) { + active.remove(name); + } + } + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); + addHistoryEventLocked(elapsedRealtime, uptime, code, name, uid); } - int mWakeLockNesting; + private void requestWakelockCpuUpdate() { + if (!mHandler.hasMessages(MSG_UPDATE_WAKELOCKS)) { + Message m = mHandler.obtainMessage(MSG_UPDATE_WAKELOCKS); + mHandler.sendMessageDelayed(m, DELAY_UPDATE_WAKELOCKS); + } + } - public void noteStartWakeLocked(int uid, int pid, String name, int type) { + public void noteStartWakeLocked(int uid, int pid, String name, String historyName, int type, + boolean unimportantForLogging, long elapsedRealtime, long uptime) { + uid = mapUid(uid); if (type == WAKE_TYPE_PARTIAL) { // Only care about partial wake locks, since full wake locks // will be canceled when the user puts the screen to sleep. + aggregateLastWakeupUptimeLocked(uptime); if (mWakeLockNesting == 0) { mHistoryCur.states |= HistoryItem.STATE_WAKE_LOCK_FLAG; if (DEBUG_HISTORY) Slog.v(TAG, "Start wake lock to: " + Integer.toHexString(mHistoryCur.states)); - addHistoryRecordLocked(SystemClock.elapsedRealtime()); + mHistoryCur.wakelockTag = mHistoryCur.localWakelockTag; + mHistoryCur.wakelockTag.string = historyName != null ? historyName : name; + mHistoryCur.wakelockTag.uid = uid; + mWakeLockImportant = !unimportantForLogging; + addHistoryRecordLocked(elapsedRealtime, uptime); + } else if (!mWakeLockImportant && !unimportantForLogging) { + if (mHistoryLastWritten.wakelockTag != null) { + // We'll try to update the last tag. + mHistoryLastWritten.wakelockTag = null; + mHistoryCur.wakelockTag = mHistoryCur.localWakelockTag; + mHistoryCur.wakelockTag.string = historyName != null ? historyName : name; + mHistoryCur.wakelockTag.uid = uid; + addHistoryRecordLocked(elapsedRealtime, uptime); + } + mWakeLockImportant = true; } mWakeLockNesting++; } if (uid >= 0) { - if (!mHandler.hasMessages(MSG_UPDATE_WAKELOCKS)) { - Message m = mHandler.obtainMessage(MSG_UPDATE_WAKELOCKS); - mHandler.sendMessageDelayed(m, DELAY_UPDATE_WAKELOCKS); - } - getUidStatsLocked(uid).noteStartWakeLocked(pid, name, type); + //if (uid == 0) { + // Slog.wtf(TAG, "Acquiring wake lock from root: " + name); + //} + requestWakelockCpuUpdate(); + getUidStatsLocked(uid).noteStartWakeLocked(pid, name, type, elapsedRealtime); } } - public void noteStopWakeLocked(int uid, int pid, String name, int type) { + public void noteStopWakeLocked(int uid, int pid, String name, int type, long elapsedRealtime, + long uptime) { + uid = mapUid(uid); if (type == WAKE_TYPE_PARTIAL) { mWakeLockNesting--; if (mWakeLockNesting == 0) { mHistoryCur.states &= ~HistoryItem.STATE_WAKE_LOCK_FLAG; if (DEBUG_HISTORY) Slog.v(TAG, "Stop wake lock to: " + Integer.toHexString(mHistoryCur.states)); - addHistoryRecordLocked(SystemClock.elapsedRealtime()); + addHistoryRecordLocked(elapsedRealtime, uptime); } } if (uid >= 0) { - if (!mHandler.hasMessages(MSG_UPDATE_WAKELOCKS)) { - Message m = mHandler.obtainMessage(MSG_UPDATE_WAKELOCKS); - mHandler.sendMessageDelayed(m, DELAY_UPDATE_WAKELOCKS); - } - getUidStatsLocked(uid).noteStopWakeLocked(pid, name, type); + requestWakelockCpuUpdate(); + getUidStatsLocked(uid).noteStopWakeLocked(pid, name, type, elapsedRealtime); } } - public void noteStartWakeFromSourceLocked(WorkSource ws, int pid, String name, int type) { - int N = ws.size(); + public void noteStartWakeFromSourceLocked(WorkSource ws, int pid, String name, + String historyName, int type, boolean unimportantForLogging) { + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); + final int N = ws.size(); for (int i=0; i<N; i++) { - noteStartWakeLocked(ws.get(i), pid, name, type); + noteStartWakeLocked(ws.get(i), pid, name, historyName, type, unimportantForLogging, + elapsedRealtime, uptime); + } + } + + public void noteChangeWakelockFromSourceLocked(WorkSource ws, int pid, String name, int type, + WorkSource newWs, int newPid, String newName, + String newHistoryName, int newType, boolean newUnimportantForLogging) { + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); + // For correct semantics, we start the need worksources first, so that we won't + // make inappropriate history items as if all wake locks went away and new ones + // appeared. This is okay because tracking of wake locks allows nesting. + final int NN = newWs.size(); + for (int i=0; i<NN; i++) { + noteStartWakeLocked(newWs.get(i), newPid, newName, newHistoryName, newType, + newUnimportantForLogging, elapsedRealtime, uptime); + } + final int NO = ws.size(); + for (int i=0; i<NO; i++) { + noteStopWakeLocked(ws.get(i), pid, name, type, elapsedRealtime, uptime); } } public void noteStopWakeFromSourceLocked(WorkSource ws, int pid, String name, int type) { - int N = ws.size(); + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); + final int N = ws.size(); for (int i=0; i<N; i++) { - noteStopWakeLocked(ws.get(i), pid, name, type); + noteStopWakeLocked(ws.get(i), pid, name, type, elapsedRealtime, uptime); + } + } + + void aggregateLastWakeupUptimeLocked(long uptimeMs) { + if (mLastWakeupReason != null) { + long deltaUptime = uptimeMs - mLastWakeupUptimeMs; + LongSamplingCounter timer = getWakeupReasonCounterLocked(mLastWakeupReason); + timer.addCountLocked(deltaUptime); + mLastWakeupReason = null; } } + public void noteWakeupReasonLocked(String reason) { + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); + if (DEBUG_HISTORY) Slog.v(TAG, "Wakeup reason reason \"" + reason +"\": " + + Integer.toHexString(mHistoryCur.states)); + aggregateLastWakeupUptimeLocked(uptime); + mHistoryCur.wakeReasonTag = mHistoryCur.localWakeReasonTag; + mHistoryCur.wakeReasonTag.string = reason; + mHistoryCur.wakeReasonTag.uid = 0; + mLastWakeupReason = reason; + mLastWakeupUptimeMs = uptime; + addHistoryRecordLocked(elapsedRealtime, uptime); + } + public int startAddingCpuLocked() { mHandler.removeMessages(MSG_UPDATE_WAKELOCKS); - if (mScreenOn) { - return 0; - } - final int N = mPartialTimers.size(); if (N == 0) { mLastPartialTimers.clear(); + mDistributeWakelockCpu = false; + return 0; + } + + if (!mOnBatteryScreenOffTimeBase.isRunning() && !mDistributeWakelockCpu) { return 0; } + mDistributeWakelockCpu = false; + // How many timers should consume CPU? Only want to include ones // that have already been in the list. for (int i=0; i<N; i++) { @@ -1838,6 +2566,7 @@ public final class BatteryStatsImpl extends BatteryStats { } public void noteProcessDiedLocked(int uid, int pid) { + uid = mapUid(uid); Uid u = mUidStats.get(uid); if (u != null) { u.mPids.remove(pid); @@ -1845,17 +2574,19 @@ public final class BatteryStatsImpl extends BatteryStats { } public long getProcessWakeTime(int uid, int pid, long realtime) { + uid = mapUid(uid); Uid u = mUidStats.get(uid); if (u != null) { Uid.Pid p = u.mPids.get(pid); if (p != null) { - return p.mWakeSum + (p.mWakeStart != 0 ? (realtime - p.mWakeStart) : 0); + return p.mWakeSumMs + (p.mWakeNesting > 0 ? (realtime - p.mWakeStartMs) : 0); } } return 0; } public void reportExcessiveWakeLocked(int uid, String proc, long overTime, long usedTime) { + uid = mapUid(uid); Uid u = mUidStats.get(uid); if (u != null) { u.reportExcessiveWakeLocked(proc, overTime, usedTime); @@ -1863,6 +2594,7 @@ public final class BatteryStatsImpl extends BatteryStats { } public void reportExcessiveCpuLocked(int uid, String proc, long overTime, long usedTime) { + uid = mapUid(uid); Uid u = mUidStats.get(uid); if (u != null) { u.reportExcessiveCpuLocked(proc, overTime, usedTime); @@ -1872,67 +2604,85 @@ public final class BatteryStatsImpl extends BatteryStats { int mSensorNesting; public void noteStartSensorLocked(int uid, int sensor) { + uid = mapUid(uid); + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); if (mSensorNesting == 0) { mHistoryCur.states |= HistoryItem.STATE_SENSOR_ON_FLAG; if (DEBUG_HISTORY) Slog.v(TAG, "Start sensor to: " + Integer.toHexString(mHistoryCur.states)); - addHistoryRecordLocked(SystemClock.elapsedRealtime()); + addHistoryRecordLocked(elapsedRealtime, uptime); } mSensorNesting++; - getUidStatsLocked(uid).noteStartSensor(sensor); + getUidStatsLocked(uid).noteStartSensor(sensor, elapsedRealtime); } public void noteStopSensorLocked(int uid, int sensor) { + uid = mapUid(uid); + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); mSensorNesting--; if (mSensorNesting == 0) { mHistoryCur.states &= ~HistoryItem.STATE_SENSOR_ON_FLAG; if (DEBUG_HISTORY) Slog.v(TAG, "Stop sensor to: " + Integer.toHexString(mHistoryCur.states)); - addHistoryRecordLocked(SystemClock.elapsedRealtime()); + addHistoryRecordLocked(elapsedRealtime, uptime); } - getUidStatsLocked(uid).noteStopSensor(sensor); + getUidStatsLocked(uid).noteStopSensor(sensor, elapsedRealtime); } int mGpsNesting; public void noteStartGpsLocked(int uid) { + uid = mapUid(uid); + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); if (mGpsNesting == 0) { mHistoryCur.states |= HistoryItem.STATE_GPS_ON_FLAG; if (DEBUG_HISTORY) Slog.v(TAG, "Start GPS to: " + Integer.toHexString(mHistoryCur.states)); - addHistoryRecordLocked(SystemClock.elapsedRealtime()); + addHistoryRecordLocked(elapsedRealtime, uptime); } mGpsNesting++; - getUidStatsLocked(uid).noteStartGps(); + getUidStatsLocked(uid).noteStartGps(elapsedRealtime); } public void noteStopGpsLocked(int uid) { + uid = mapUid(uid); + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); mGpsNesting--; if (mGpsNesting == 0) { mHistoryCur.states &= ~HistoryItem.STATE_GPS_ON_FLAG; if (DEBUG_HISTORY) Slog.v(TAG, "Stop GPS to: " + Integer.toHexString(mHistoryCur.states)); - addHistoryRecordLocked(SystemClock.elapsedRealtime()); + addHistoryRecordLocked(elapsedRealtime, uptime); } - getUidStatsLocked(uid).noteStopGps(); + getUidStatsLocked(uid).noteStopGps(elapsedRealtime); } public void noteScreenOnLocked() { if (!mScreenOn) { + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); mHistoryCur.states |= HistoryItem.STATE_SCREEN_ON_FLAG; if (DEBUG_HISTORY) Slog.v(TAG, "Screen on to: " + Integer.toHexString(mHistoryCur.states)); - addHistoryRecordLocked(SystemClock.elapsedRealtime()); + addHistoryRecordLocked(elapsedRealtime, uptime); mScreenOn = true; - mScreenOnTimer.startRunningLocked(this); + mScreenOnTimer.startRunningLocked(elapsedRealtime); if (mScreenBrightnessBin >= 0) { - mScreenBrightnessTimer[mScreenBrightnessBin].startRunningLocked(this); + mScreenBrightnessTimer[mScreenBrightnessBin].startRunningLocked(elapsedRealtime); } + updateTimeBasesLocked(mOnBatteryTimeBase.isRunning(), false, + SystemClock.uptimeMillis() * 1000, elapsedRealtime * 1000); + // Fake a wake lock, so we consider the device waked as long // as the screen is on. - noteStartWakeLocked(-1, -1, "dummy", WAKE_TYPE_PARTIAL); - + noteStartWakeLocked(-1, -1, "screen", null, WAKE_TYPE_PARTIAL, false, + elapsedRealtime, uptime); + // Update discharge amounts. if (mOnBatteryInternal) { updateDischargeScreenLevelsLocked(false, true); @@ -1942,18 +2692,24 @@ public final class BatteryStatsImpl extends BatteryStats { public void noteScreenOffLocked() { if (mScreenOn) { + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); mHistoryCur.states &= ~HistoryItem.STATE_SCREEN_ON_FLAG; if (DEBUG_HISTORY) Slog.v(TAG, "Screen off to: " + Integer.toHexString(mHistoryCur.states)); - addHistoryRecordLocked(SystemClock.elapsedRealtime()); + addHistoryRecordLocked(elapsedRealtime, uptime); mScreenOn = false; - mScreenOnTimer.stopRunningLocked(this); + mScreenOnTimer.stopRunningLocked(elapsedRealtime); if (mScreenBrightnessBin >= 0) { - mScreenBrightnessTimer[mScreenBrightnessBin].stopRunningLocked(this); + mScreenBrightnessTimer[mScreenBrightnessBin].stopRunningLocked(elapsedRealtime); } - noteStopWakeLocked(-1, -1, "dummy", WAKE_TYPE_PARTIAL); - + noteStopWakeLocked(-1, -1, "screen", WAKE_TYPE_PARTIAL, + elapsedRealtime, uptime); + + updateTimeBasesLocked(mOnBatteryTimeBase.isRunning(), true, + SystemClock.uptimeMillis() * 1000, elapsedRealtime * 1000); + // Update discharge amounts. if (mOnBatteryInternal) { updateDischargeScreenLevelsLocked(true, false); @@ -1967,16 +2723,18 @@ public final class BatteryStatsImpl extends BatteryStats { if (bin < 0) bin = 0; else if (bin >= NUM_SCREEN_BRIGHTNESS_BINS) bin = NUM_SCREEN_BRIGHTNESS_BINS-1; if (mScreenBrightnessBin != bin) { + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); mHistoryCur.states = (mHistoryCur.states&~HistoryItem.STATE_BRIGHTNESS_MASK) | (bin << HistoryItem.STATE_BRIGHTNESS_SHIFT); if (DEBUG_HISTORY) Slog.v(TAG, "Screen brightness " + bin + " to: " + Integer.toHexString(mHistoryCur.states)); - addHistoryRecordLocked(SystemClock.elapsedRealtime()); + addHistoryRecordLocked(elapsedRealtime, uptime); if (mScreenOn) { if (mScreenBrightnessBin >= 0) { - mScreenBrightnessTimer[mScreenBrightnessBin].stopRunningLocked(this); + mScreenBrightnessTimer[mScreenBrightnessBin].stopRunningLocked(elapsedRealtime); } - mScreenBrightnessTimer[bin].startRunningLocked(this); + mScreenBrightnessTimer[bin].startRunningLocked(elapsedRealtime); } mScreenBrightnessBin = bin; } @@ -1987,38 +2745,85 @@ public final class BatteryStatsImpl extends BatteryStats { } public void noteUserActivityLocked(int uid, int event) { - getUidStatsLocked(uid).noteUserActivityLocked(event); + if (mOnBatteryInternal) { + uid = mapUid(uid); + getUidStatsLocked(uid).noteUserActivityLocked(event); + } + } + + public void noteMobileRadioPowerState(int powerState, long timestampNs) { + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); + if (mMobileRadioPowerState != powerState) { + long realElapsedRealtimeMs; + final boolean active = + powerState == DataConnectionRealTimeInfo.DC_POWER_STATE_MEDIUM + || powerState == DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH; + if (active) { + realElapsedRealtimeMs = elapsedRealtime; + mHistoryCur.states |= HistoryItem.STATE_MOBILE_RADIO_ACTIVE_FLAG; + } else { + realElapsedRealtimeMs = timestampNs / (1000*1000); + long lastUpdateTimeMs = mMobileRadioActiveTimer.getLastUpdateTimeMs(); + if (realElapsedRealtimeMs < lastUpdateTimeMs) { + Slog.wtf(TAG, "Data connection inactive timestamp " + realElapsedRealtimeMs + + " is before start time " + lastUpdateTimeMs); + realElapsedRealtimeMs = elapsedRealtime; + } else if (realElapsedRealtimeMs < elapsedRealtime) { + mMobileRadioActiveAdjustedTime.addCountLocked(elapsedRealtime + - realElapsedRealtimeMs); + } + mHistoryCur.states &= ~HistoryItem.STATE_MOBILE_RADIO_ACTIVE_FLAG; + } + if (DEBUG_HISTORY) Slog.v(TAG, "Mobile network active " + active + " to: " + + Integer.toHexString(mHistoryCur.states)); + addHistoryRecordLocked(elapsedRealtime, uptime); + mMobileRadioPowerState = powerState; + if (active) { + mMobileRadioActiveTimer.startRunningLocked(elapsedRealtime); + mMobileRadioActivePerAppTimer.startRunningLocked(elapsedRealtime); + } else { + mMobileRadioActiveTimer.stopRunningLocked(realElapsedRealtimeMs); + updateNetworkActivityLocked(NET_UPDATE_MOBILE, realElapsedRealtimeMs); + mMobileRadioActivePerAppTimer.stopRunningLocked(realElapsedRealtimeMs); + } + } } public void notePhoneOnLocked() { if (!mPhoneOn) { + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); mHistoryCur.states |= HistoryItem.STATE_PHONE_IN_CALL_FLAG; if (DEBUG_HISTORY) Slog.v(TAG, "Phone on to: " + Integer.toHexString(mHistoryCur.states)); - addHistoryRecordLocked(SystemClock.elapsedRealtime()); + addHistoryRecordLocked(elapsedRealtime, uptime); mPhoneOn = true; - mPhoneOnTimer.startRunningLocked(this); + mPhoneOnTimer.startRunningLocked(elapsedRealtime); } } public void notePhoneOffLocked() { if (mPhoneOn) { + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); mHistoryCur.states &= ~HistoryItem.STATE_PHONE_IN_CALL_FLAG; if (DEBUG_HISTORY) Slog.v(TAG, "Phone off to: " + Integer.toHexString(mHistoryCur.states)); - addHistoryRecordLocked(SystemClock.elapsedRealtime()); + addHistoryRecordLocked(elapsedRealtime, uptime); mPhoneOn = false; - mPhoneOnTimer.stopRunningLocked(this); + mPhoneOnTimer.stopRunningLocked(elapsedRealtime); } } void stopAllSignalStrengthTimersLocked(int except) { + final long elapsedRealtime = SystemClock.elapsedRealtime(); for (int i = 0; i < SignalStrength.NUM_SIGNAL_STRENGTH_BINS; i++) { if (i == except) { continue; } while (mPhoneSignalStrengthsTimer[i].isRunningLocked()) { - mPhoneSignalStrengthsTimer[i].stopRunningLocked(this); + mPhoneSignalStrengthsTimer[i].stopRunningLocked(elapsedRealtime); } } } @@ -2036,26 +2841,29 @@ public final class BatteryStatsImpl extends BatteryStats { return state; } - private void updateAllPhoneStateLocked(int state, int simState, int bin) { + private void updateAllPhoneStateLocked(int state, int simState, int strengthBin) { boolean scanning = false; boolean newHistory = false; mPhoneServiceStateRaw = state; mPhoneSimStateRaw = simState; - mPhoneSignalStrengthBinRaw = bin; + mPhoneSignalStrengthBinRaw = strengthBin; + + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); if (simState == TelephonyManager.SIM_STATE_ABSENT) { // In this case we will always be STATE_OUT_OF_SERVICE, so need // to infer that we are scanning from other data. if (state == ServiceState.STATE_OUT_OF_SERVICE - && bin > SignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN) { + && strengthBin > SignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN) { state = ServiceState.STATE_IN_SERVICE; } } // If the phone is powered off, stop all timers. if (state == ServiceState.STATE_POWER_OFF) { - bin = -1; + strengthBin = -1; // If we are in service, make sure the correct signal string timer is running. } else if (state == ServiceState.STATE_IN_SERVICE) { @@ -2065,13 +2873,13 @@ public final class BatteryStatsImpl extends BatteryStats { // bin and have the scanning bit set. } else if (state == ServiceState.STATE_OUT_OF_SERVICE) { scanning = true; - bin = SignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN; + strengthBin = SignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN; if (!mPhoneSignalScanningTimer.isRunningLocked()) { mHistoryCur.states |= HistoryItem.STATE_PHONE_SCANNING_FLAG; newHistory = true; if (DEBUG_HISTORY) Slog.v(TAG, "Phone started scanning to: " + Integer.toHexString(mHistoryCur.states)); - mPhoneSignalScanningTimer.startRunningLocked(this); + mPhoneSignalScanningTimer.startRunningLocked(elapsedRealtime); } } @@ -2082,7 +2890,7 @@ public final class BatteryStatsImpl extends BatteryStats { if (DEBUG_HISTORY) Slog.v(TAG, "Phone stopped scanning to: " + Integer.toHexString(mHistoryCur.states)); newHistory = true; - mPhoneSignalScanningTimer.stopRunningLocked(this); + mPhoneSignalScanningTimer.stopRunningLocked(elapsedRealtime); } } @@ -2095,27 +2903,28 @@ public final class BatteryStatsImpl extends BatteryStats { mPhoneServiceState = state; } - if (mPhoneSignalStrengthBin != bin) { + if (mPhoneSignalStrengthBin != strengthBin) { if (mPhoneSignalStrengthBin >= 0) { - mPhoneSignalStrengthsTimer[mPhoneSignalStrengthBin].stopRunningLocked(this); + mPhoneSignalStrengthsTimer[mPhoneSignalStrengthBin].stopRunningLocked( + elapsedRealtime); } - if (bin >= 0) { - if (!mPhoneSignalStrengthsTimer[bin].isRunningLocked()) { - mPhoneSignalStrengthsTimer[bin].startRunningLocked(this); + if (strengthBin >= 0) { + if (!mPhoneSignalStrengthsTimer[strengthBin].isRunningLocked()) { + mPhoneSignalStrengthsTimer[strengthBin].startRunningLocked(elapsedRealtime); } mHistoryCur.states = (mHistoryCur.states&~HistoryItem.STATE_SIGNAL_STRENGTH_MASK) - | (bin << HistoryItem.STATE_SIGNAL_STRENGTH_SHIFT); - if (DEBUG_HISTORY) Slog.v(TAG, "Signal strength " + bin + " to: " + | (strengthBin << HistoryItem.STATE_SIGNAL_STRENGTH_SHIFT); + if (DEBUG_HISTORY) Slog.v(TAG, "Signal strength " + strengthBin + " to: " + Integer.toHexString(mHistoryCur.states)); newHistory = true; } else { stopAllSignalStrengthTimersLocked(-1); } - mPhoneSignalStrengthBin = bin; + mPhoneSignalStrengthBin = strengthBin; } if (newHistory) { - addHistoryRecordLocked(SystemClock.elapsedRealtime()); + addHistoryRecordLocked(elapsedRealtime, uptime); } } @@ -2189,120 +2998,142 @@ public final class BatteryStatsImpl extends BatteryStats { } if (DEBUG) Log.i(TAG, "Phone Data Connection -> " + dataType + " = " + hasData); if (mPhoneDataConnectionType != bin) { + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); mHistoryCur.states = (mHistoryCur.states&~HistoryItem.STATE_DATA_CONNECTION_MASK) | (bin << HistoryItem.STATE_DATA_CONNECTION_SHIFT); if (DEBUG_HISTORY) Slog.v(TAG, "Data connection " + bin + " to: " + Integer.toHexString(mHistoryCur.states)); - addHistoryRecordLocked(SystemClock.elapsedRealtime()); + addHistoryRecordLocked(elapsedRealtime, uptime); if (mPhoneDataConnectionType >= 0) { - mPhoneDataConnectionsTimer[mPhoneDataConnectionType].stopRunningLocked(this); + mPhoneDataConnectionsTimer[mPhoneDataConnectionType].stopRunningLocked( + elapsedRealtime); } mPhoneDataConnectionType = bin; - mPhoneDataConnectionsTimer[bin].startRunningLocked(this); + mPhoneDataConnectionsTimer[bin].startRunningLocked(elapsedRealtime); } } public void noteWifiOnLocked() { if (!mWifiOn) { + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); mHistoryCur.states |= HistoryItem.STATE_WIFI_ON_FLAG; if (DEBUG_HISTORY) Slog.v(TAG, "WIFI on to: " + Integer.toHexString(mHistoryCur.states)); - addHistoryRecordLocked(SystemClock.elapsedRealtime()); + addHistoryRecordLocked(elapsedRealtime, uptime); mWifiOn = true; - mWifiOnTimer.startRunningLocked(this); + mWifiOnTimer.startRunningLocked(elapsedRealtime); } } public void noteWifiOffLocked() { + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); if (mWifiOn) { mHistoryCur.states &= ~HistoryItem.STATE_WIFI_ON_FLAG; if (DEBUG_HISTORY) Slog.v(TAG, "WIFI off to: " + Integer.toHexString(mHistoryCur.states)); - addHistoryRecordLocked(SystemClock.elapsedRealtime()); + addHistoryRecordLocked(elapsedRealtime, uptime); mWifiOn = false; - mWifiOnTimer.stopRunningLocked(this); - } - if (mWifiOnUid >= 0) { - getUidStatsLocked(mWifiOnUid).noteWifiStoppedLocked(); - mWifiOnUid = -1; + mWifiOnTimer.stopRunningLocked(elapsedRealtime); } } public void noteAudioOnLocked(int uid) { + uid = mapUid(uid); + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); if (!mAudioOn) { mHistoryCur.states |= HistoryItem.STATE_AUDIO_ON_FLAG; if (DEBUG_HISTORY) Slog.v(TAG, "Audio on to: " + Integer.toHexString(mHistoryCur.states)); - addHistoryRecordLocked(SystemClock.elapsedRealtime()); + addHistoryRecordLocked(elapsedRealtime, uptime); mAudioOn = true; - mAudioOnTimer.startRunningLocked(this); + mAudioOnTimer.startRunningLocked(elapsedRealtime); } - getUidStatsLocked(uid).noteAudioTurnedOnLocked(); + getUidStatsLocked(uid).noteAudioTurnedOnLocked(elapsedRealtime); } public void noteAudioOffLocked(int uid) { + uid = mapUid(uid); + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); if (mAudioOn) { mHistoryCur.states &= ~HistoryItem.STATE_AUDIO_ON_FLAG; if (DEBUG_HISTORY) Slog.v(TAG, "Audio off to: " + Integer.toHexString(mHistoryCur.states)); - addHistoryRecordLocked(SystemClock.elapsedRealtime()); + addHistoryRecordLocked(elapsedRealtime, uptime); mAudioOn = false; - mAudioOnTimer.stopRunningLocked(this); + mAudioOnTimer.stopRunningLocked(elapsedRealtime); } - getUidStatsLocked(uid).noteAudioTurnedOffLocked(); + getUidStatsLocked(uid).noteAudioTurnedOffLocked(elapsedRealtime); } public void noteVideoOnLocked(int uid) { + uid = mapUid(uid); + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); if (!mVideoOn) { - mHistoryCur.states |= HistoryItem.STATE_VIDEO_ON_FLAG; + mHistoryCur.states2 |= HistoryItem.STATE2_VIDEO_ON_FLAG; if (DEBUG_HISTORY) Slog.v(TAG, "Video on to: " + Integer.toHexString(mHistoryCur.states)); - addHistoryRecordLocked(SystemClock.elapsedRealtime()); + addHistoryRecordLocked(elapsedRealtime, uptime); mVideoOn = true; - mVideoOnTimer.startRunningLocked(this); + mVideoOnTimer.startRunningLocked(elapsedRealtime); } - getUidStatsLocked(uid).noteVideoTurnedOnLocked(); + getUidStatsLocked(uid).noteVideoTurnedOnLocked(elapsedRealtime); } public void noteVideoOffLocked(int uid) { + uid = mapUid(uid); + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); if (mVideoOn) { - mHistoryCur.states &= ~HistoryItem.STATE_VIDEO_ON_FLAG; + mHistoryCur.states2 &= ~HistoryItem.STATE2_VIDEO_ON_FLAG; if (DEBUG_HISTORY) Slog.v(TAG, "Video off to: " + Integer.toHexString(mHistoryCur.states)); - addHistoryRecordLocked(SystemClock.elapsedRealtime()); + addHistoryRecordLocked(elapsedRealtime, uptime); mVideoOn = false; - mVideoOnTimer.stopRunningLocked(this); + mVideoOnTimer.stopRunningLocked(elapsedRealtime); } - getUidStatsLocked(uid).noteVideoTurnedOffLocked(); + getUidStatsLocked(uid).noteVideoTurnedOffLocked(elapsedRealtime); } public void noteActivityResumedLocked(int uid) { - getUidStatsLocked(uid).noteActivityResumedLocked(); + uid = mapUid(uid); + getUidStatsLocked(uid).noteActivityResumedLocked(SystemClock.elapsedRealtime()); } public void noteActivityPausedLocked(int uid) { - getUidStatsLocked(uid).noteActivityPausedLocked(); + uid = mapUid(uid); + getUidStatsLocked(uid).noteActivityPausedLocked(SystemClock.elapsedRealtime()); } public void noteVibratorOnLocked(int uid, long durationMillis) { + uid = mapUid(uid); getUidStatsLocked(uid).noteVibratorOnLocked(durationMillis); } public void noteVibratorOffLocked(int uid) { + uid = mapUid(uid); getUidStatsLocked(uid).noteVibratorOffLocked(); } public void noteWifiRunningLocked(WorkSource ws) { if (!mGlobalWifiRunning) { + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); mHistoryCur.states |= HistoryItem.STATE_WIFI_RUNNING_FLAG; if (DEBUG_HISTORY) Slog.v(TAG, "WIFI running to: " + Integer.toHexString(mHistoryCur.states)); - addHistoryRecordLocked(SystemClock.elapsedRealtime()); + addHistoryRecordLocked(elapsedRealtime, uptime); mGlobalWifiRunning = true; - mGlobalWifiRunningTimer.startRunningLocked(this); + mGlobalWifiRunningTimer.startRunningLocked(elapsedRealtime); int N = ws.size(); for (int i=0; i<N; i++) { - getUidStatsLocked(ws.get(i)).noteWifiRunningLocked(); + int uid = mapUid(ws.get(i)); + getUidStatsLocked(uid).noteWifiRunningLocked(elapsedRealtime); } } else { Log.w(TAG, "noteWifiRunningLocked -- called while WIFI running"); @@ -2311,13 +3142,16 @@ public final class BatteryStatsImpl extends BatteryStats { public void noteWifiRunningChangedLocked(WorkSource oldWs, WorkSource newWs) { if (mGlobalWifiRunning) { + final long elapsedRealtime = SystemClock.elapsedRealtime(); int N = oldWs.size(); for (int i=0; i<N; i++) { - getUidStatsLocked(oldWs.get(i)).noteWifiStoppedLocked(); + int uid = mapUid(oldWs.get(i)); + getUidStatsLocked(uid).noteWifiStoppedLocked(elapsedRealtime); } N = newWs.size(); for (int i=0; i<N; i++) { - getUidStatsLocked(newWs.get(i)).noteWifiRunningLocked(); + int uid = mapUid(newWs.get(i)); + getUidStatsLocked(uid).noteWifiRunningLocked(elapsedRealtime); } } else { Log.w(TAG, "noteWifiRunningChangedLocked -- called while WIFI not running"); @@ -2326,121 +3160,174 @@ public final class BatteryStatsImpl extends BatteryStats { public void noteWifiStoppedLocked(WorkSource ws) { if (mGlobalWifiRunning) { + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); mHistoryCur.states &= ~HistoryItem.STATE_WIFI_RUNNING_FLAG; if (DEBUG_HISTORY) Slog.v(TAG, "WIFI stopped to: " + Integer.toHexString(mHistoryCur.states)); - addHistoryRecordLocked(SystemClock.elapsedRealtime()); + addHistoryRecordLocked(elapsedRealtime, uptime); mGlobalWifiRunning = false; - mGlobalWifiRunningTimer.stopRunningLocked(this); + mGlobalWifiRunningTimer.stopRunningLocked(elapsedRealtime); int N = ws.size(); for (int i=0; i<N; i++) { - getUidStatsLocked(ws.get(i)).noteWifiStoppedLocked(); + int uid = mapUid(ws.get(i)); + getUidStatsLocked(uid).noteWifiStoppedLocked(elapsedRealtime); } } else { Log.w(TAG, "noteWifiStoppedLocked -- called while WIFI not running"); } } + public void noteWifiStateLocked(int wifiState, String accessPoint) { + if (DEBUG) Log.i(TAG, "WiFi state -> " + wifiState); + if (mWifiState != wifiState) { + final long elapsedRealtime = SystemClock.elapsedRealtime(); + if (mWifiState >= 0) { + mWifiStateTimer[mWifiState].stopRunningLocked(elapsedRealtime); + } + mWifiState = wifiState; + mWifiStateTimer[wifiState].startRunningLocked(elapsedRealtime); + } + } + public void noteBluetoothOnLocked() { if (!mBluetoothOn) { + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); mHistoryCur.states |= HistoryItem.STATE_BLUETOOTH_ON_FLAG; if (DEBUG_HISTORY) Slog.v(TAG, "Bluetooth on to: " + Integer.toHexString(mHistoryCur.states)); - addHistoryRecordLocked(SystemClock.elapsedRealtime()); + addHistoryRecordLocked(elapsedRealtime, uptime); mBluetoothOn = true; - mBluetoothOnTimer.startRunningLocked(this); + mBluetoothOnTimer.startRunningLocked(elapsedRealtime); } } public void noteBluetoothOffLocked() { if (mBluetoothOn) { + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); mHistoryCur.states &= ~HistoryItem.STATE_BLUETOOTH_ON_FLAG; if (DEBUG_HISTORY) Slog.v(TAG, "Bluetooth off to: " + Integer.toHexString(mHistoryCur.states)); - addHistoryRecordLocked(SystemClock.elapsedRealtime()); + addHistoryRecordLocked(elapsedRealtime, uptime); mBluetoothOn = false; - mBluetoothOnTimer.stopRunningLocked(this); + mBluetoothOnTimer.stopRunningLocked(elapsedRealtime); + } + } + + public void noteBluetoothStateLocked(int bluetoothState) { + if (DEBUG) Log.i(TAG, "Bluetooth state -> " + bluetoothState); + if (mBluetoothState != bluetoothState) { + final long elapsedRealtime = SystemClock.elapsedRealtime(); + if (mBluetoothState >= 0) { + mBluetoothStateTimer[mBluetoothState].stopRunningLocked(elapsedRealtime); + } + mBluetoothState = bluetoothState; + mBluetoothStateTimer[bluetoothState].startRunningLocked(elapsedRealtime); } } int mWifiFullLockNesting = 0; public void noteFullWifiLockAcquiredLocked(int uid) { + uid = mapUid(uid); + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); if (mWifiFullLockNesting == 0) { mHistoryCur.states |= HistoryItem.STATE_WIFI_FULL_LOCK_FLAG; if (DEBUG_HISTORY) Slog.v(TAG, "WIFI full lock on to: " + Integer.toHexString(mHistoryCur.states)); - addHistoryRecordLocked(SystemClock.elapsedRealtime()); + addHistoryRecordLocked(elapsedRealtime, uptime); } mWifiFullLockNesting++; - getUidStatsLocked(uid).noteFullWifiLockAcquiredLocked(); + getUidStatsLocked(uid).noteFullWifiLockAcquiredLocked(elapsedRealtime); } public void noteFullWifiLockReleasedLocked(int uid) { + uid = mapUid(uid); + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); mWifiFullLockNesting--; if (mWifiFullLockNesting == 0) { mHistoryCur.states &= ~HistoryItem.STATE_WIFI_FULL_LOCK_FLAG; if (DEBUG_HISTORY) Slog.v(TAG, "WIFI full lock off to: " + Integer.toHexString(mHistoryCur.states)); - addHistoryRecordLocked(SystemClock.elapsedRealtime()); + addHistoryRecordLocked(elapsedRealtime, uptime); } - getUidStatsLocked(uid).noteFullWifiLockReleasedLocked(); + getUidStatsLocked(uid).noteFullWifiLockReleasedLocked(elapsedRealtime); } int mWifiScanNesting = 0; public void noteWifiScanStartedLocked(int uid) { + uid = mapUid(uid); + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); if (mWifiScanNesting == 0) { mHistoryCur.states |= HistoryItem.STATE_WIFI_SCAN_FLAG; if (DEBUG_HISTORY) Slog.v(TAG, "WIFI scan started for: " + Integer.toHexString(mHistoryCur.states)); - addHistoryRecordLocked(SystemClock.elapsedRealtime()); + addHistoryRecordLocked(elapsedRealtime, uptime); } mWifiScanNesting++; - getUidStatsLocked(uid).noteWifiScanStartedLocked(); + getUidStatsLocked(uid).noteWifiScanStartedLocked(elapsedRealtime); } public void noteWifiScanStoppedLocked(int uid) { + uid = mapUid(uid); + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); mWifiScanNesting--; if (mWifiScanNesting == 0) { mHistoryCur.states &= ~HistoryItem.STATE_WIFI_SCAN_FLAG; if (DEBUG_HISTORY) Slog.v(TAG, "WIFI scan stopped for: " + Integer.toHexString(mHistoryCur.states)); - addHistoryRecordLocked(SystemClock.elapsedRealtime()); + addHistoryRecordLocked(elapsedRealtime, uptime); } - getUidStatsLocked(uid).noteWifiScanStoppedLocked(); + getUidStatsLocked(uid).noteWifiScanStoppedLocked(elapsedRealtime); } public void noteWifiBatchedScanStartedLocked(int uid, int csph) { - getUidStatsLocked(uid).noteWifiBatchedScanStartedLocked(csph); + uid = mapUid(uid); + final long elapsedRealtime = SystemClock.elapsedRealtime(); + getUidStatsLocked(uid).noteWifiBatchedScanStartedLocked(csph, elapsedRealtime); } public void noteWifiBatchedScanStoppedLocked(int uid) { - getUidStatsLocked(uid).noteWifiBatchedScanStoppedLocked(); + uid = mapUid(uid); + final long elapsedRealtime = SystemClock.elapsedRealtime(); + getUidStatsLocked(uid).noteWifiBatchedScanStoppedLocked(elapsedRealtime); } int mWifiMulticastNesting = 0; public void noteWifiMulticastEnabledLocked(int uid) { + uid = mapUid(uid); + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); if (mWifiMulticastNesting == 0) { mHistoryCur.states |= HistoryItem.STATE_WIFI_MULTICAST_ON_FLAG; if (DEBUG_HISTORY) Slog.v(TAG, "WIFI multicast on to: " + Integer.toHexString(mHistoryCur.states)); - addHistoryRecordLocked(SystemClock.elapsedRealtime()); + addHistoryRecordLocked(elapsedRealtime, uptime); } mWifiMulticastNesting++; - getUidStatsLocked(uid).noteWifiMulticastEnabledLocked(); + getUidStatsLocked(uid).noteWifiMulticastEnabledLocked(elapsedRealtime); } public void noteWifiMulticastDisabledLocked(int uid) { + uid = mapUid(uid); + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); mWifiMulticastNesting--; if (mWifiMulticastNesting == 0) { mHistoryCur.states &= ~HistoryItem.STATE_WIFI_MULTICAST_ON_FLAG; if (DEBUG_HISTORY) Slog.v(TAG, "WIFI multicast off to: " + Integer.toHexString(mHistoryCur.states)); - addHistoryRecordLocked(SystemClock.elapsedRealtime()); + addHistoryRecordLocked(elapsedRealtime, uptime); } - getUidStatsLocked(uid).noteWifiMulticastDisabledLocked(); + getUidStatsLocked(uid).noteWifiMulticastDisabledLocked(elapsedRealtime); } public void noteFullWifiLockAcquiredFromSourceLocked(WorkSource ws) { @@ -2499,16 +3386,45 @@ public final class BatteryStatsImpl extends BatteryStats { } } + private static String[] includeInStringArray(String[] array, String str) { + if (ArrayUtils.indexOf(array, str) >= 0) { + return array; + } + String[] newArray = new String[array.length+1]; + System.arraycopy(array, 0, newArray, 0, array.length); + newArray[array.length] = str; + return newArray; + } + + private static String[] excludeFromStringArray(String[] array, String str) { + int index = ArrayUtils.indexOf(array, str); + if (index >= 0) { + String[] newArray = new String[array.length-1]; + if (index > 0) { + System.arraycopy(array, 0, newArray, 0, index); + } + if (index < array.length-1) { + System.arraycopy(array, index+1, newArray, index, array.length-index-1); + } + return newArray; + } + return array; + } + public void noteNetworkInterfaceTypeLocked(String iface, int networkType) { if (ConnectivityManager.isNetworkTypeMobile(networkType)) { - mMobileIfaces.add(iface); + mMobileIfaces = includeInStringArray(mMobileIfaces, iface); + if (DEBUG) Slog.d(TAG, "Note mobile iface " + iface + ": " + mMobileIfaces); } else { - mMobileIfaces.remove(iface); + mMobileIfaces = excludeFromStringArray(mMobileIfaces, iface); + if (DEBUG) Slog.d(TAG, "Note non-mobile iface " + iface + ": " + mMobileIfaces); } if (ConnectivityManager.isNetworkTypeWifi(networkType)) { - mWifiIfaces.add(iface); + mWifiIfaces = includeInStringArray(mWifiIfaces, iface); + if (DEBUG) Slog.d(TAG, "Note wifi iface " + iface + ": " + mWifiIfaces); } else { - mWifiIfaces.remove(iface); + mWifiIfaces = excludeFromStringArray(mWifiIfaces, iface); + if (DEBUG) Slog.d(TAG, "Note non-wifi iface " + iface + ": " + mWifiIfaces); } } @@ -2516,37 +3432,45 @@ public final class BatteryStatsImpl extends BatteryStats { // During device boot, qtaguid isn't enabled until after the inital // loading of battery stats. Now that they're enabled, take our initial // snapshot for future delta calculation. - updateNetworkActivityLocked(); + updateNetworkActivityLocked(NET_UPDATE_ALL, SystemClock.elapsedRealtime()); + } + + @Override public long getScreenOnTime(long elapsedRealtimeUs, int which) { + return mScreenOnTimer.getTotalTimeLocked(elapsedRealtimeUs, which); } - @Override public long getScreenOnTime(long batteryRealtime, int which) { - return mScreenOnTimer.getTotalTimeLocked(batteryRealtime, which); + @Override public int getScreenOnCount(int which) { + return mScreenOnTimer.getCountLocked(which); } @Override public long getScreenBrightnessTime(int brightnessBin, - long batteryRealtime, int which) { + long elapsedRealtimeUs, int which) { return mScreenBrightnessTimer[brightnessBin].getTotalTimeLocked( - batteryRealtime, which); + elapsedRealtimeUs, which); } @Override public int getInputEventCount(int which) { return mInputEventCounter.getCountLocked(which); } - @Override public long getPhoneOnTime(long batteryRealtime, int which) { - return mPhoneOnTimer.getTotalTimeLocked(batteryRealtime, which); + @Override public long getPhoneOnTime(long elapsedRealtimeUs, int which) { + return mPhoneOnTimer.getTotalTimeLocked(elapsedRealtimeUs, which); + } + + @Override public int getPhoneOnCount(int which) { + return mPhoneOnTimer.getCountLocked(which); } @Override public long getPhoneSignalStrengthTime(int strengthBin, - long batteryRealtime, int which) { + long elapsedRealtimeUs, int which) { return mPhoneSignalStrengthsTimer[strengthBin].getTotalTimeLocked( - batteryRealtime, which); + elapsedRealtimeUs, which); } @Override public long getPhoneSignalScanningTime( - long batteryRealtime, int which) { + long elapsedRealtimeUs, int which) { return mPhoneSignalScanningTimer.getTotalTimeLocked( - batteryRealtime, which); + elapsedRealtimeUs, which); } @Override public int getPhoneSignalStrengthCount(int strengthBin, int which) { @@ -2554,36 +3478,89 @@ public final class BatteryStatsImpl extends BatteryStats { } @Override public long getPhoneDataConnectionTime(int dataType, - long batteryRealtime, int which) { + long elapsedRealtimeUs, int which) { return mPhoneDataConnectionsTimer[dataType].getTotalTimeLocked( - batteryRealtime, which); + elapsedRealtimeUs, which); } @Override public int getPhoneDataConnectionCount(int dataType, int which) { return mPhoneDataConnectionsTimer[dataType].getCountLocked(which); } - @Override public long getWifiOnTime(long batteryRealtime, int which) { - return mWifiOnTimer.getTotalTimeLocked(batteryRealtime, which); + @Override public long getMobileRadioActiveTime(long elapsedRealtimeUs, int which) { + return mMobileRadioActiveTimer.getTotalTimeLocked(elapsedRealtimeUs, which); + } + + @Override public int getMobileRadioActiveCount(int which) { + return mMobileRadioActiveTimer.getCountLocked(which); + } + + @Override public long getMobileRadioActiveAdjustedTime(int which) { + return mMobileRadioActiveAdjustedTime.getCountLocked(which); + } + + @Override public long getMobileRadioActiveUnknownTime(int which) { + return mMobileRadioActiveUnknownTime.getCountLocked(which); + } + + @Override public int getMobileRadioActiveUnknownCount(int which) { + return (int)mMobileRadioActiveUnknownCount.getCountLocked(which); + } + + @Override public long getWifiOnTime(long elapsedRealtimeUs, int which) { + return mWifiOnTimer.getTotalTimeLocked(elapsedRealtimeUs, which); + } + + @Override public long getGlobalWifiRunningTime(long elapsedRealtimeUs, int which) { + return mGlobalWifiRunningTimer.getTotalTimeLocked(elapsedRealtimeUs, which); + } + + @Override public long getWifiStateTime(int wifiState, + long elapsedRealtimeUs, int which) { + return mWifiStateTimer[wifiState].getTotalTimeLocked( + elapsedRealtimeUs, which); + } + + @Override public int getWifiStateCount(int wifiState, int which) { + return mWifiStateTimer[wifiState].getCountLocked(which); } - @Override public long getGlobalWifiRunningTime(long batteryRealtime, int which) { - return mGlobalWifiRunningTimer.getTotalTimeLocked(batteryRealtime, which); + @Override public long getBluetoothOnTime(long elapsedRealtimeUs, int which) { + return mBluetoothOnTimer.getTotalTimeLocked(elapsedRealtimeUs, which); } - @Override public long getBluetoothOnTime(long batteryRealtime, int which) { - return mBluetoothOnTimer.getTotalTimeLocked(batteryRealtime, which); + @Override public long getBluetoothStateTime(int bluetoothState, + long elapsedRealtimeUs, int which) { + return mBluetoothStateTimer[bluetoothState].getTotalTimeLocked( + elapsedRealtimeUs, which); + } + + @Override public int getBluetoothStateCount(int bluetoothState, int which) { + return mBluetoothStateTimer[bluetoothState].getCountLocked(which); } @Override - public long getNetworkActivityCount(int type, int which) { - if (type >= 0 && type < mNetworkActivityCounters.length) { - return mNetworkActivityCounters[type].getCountLocked(which); + public long getNetworkActivityBytes(int type, int which) { + if (type >= 0 && type < mNetworkByteActivityCounters.length) { + return mNetworkByteActivityCounters[type].getCountLocked(which); } else { return 0; } } + @Override + public long getNetworkActivityPackets(int type, int which) { + if (type >= 0 && type < mNetworkPacketActivityCounters.length) { + return mNetworkPacketActivityCounters[type].getCountLocked(which); + } else { + return 0; + } + } + + @Override public long getStartClockTime() { + return mStartClockTime; + } + @Override public boolean getIsOnBattery() { return mOnBattery; } @@ -2627,7 +3604,10 @@ public final class BatteryStatsImpl extends BatteryStats { Counter[] mUserActivityCounters; - LongSamplingCounter[] mNetworkActivityCounters; + LongSamplingCounter[] mNetworkByteActivityCounters; + LongSamplingCounter[] mNetworkPacketActivityCounters; + LongSamplingCounter mMobileRadioActiveTime; + LongSamplingCounter mMobileRadioActiveCount; /** * The statistics we have collected for this uid's wake locks. @@ -2657,14 +3637,14 @@ public final class BatteryStatsImpl extends BatteryStats { public Uid(int uid) { mUid = uid; mWifiRunningTimer = new StopwatchTimer(Uid.this, WIFI_RUNNING, - mWifiRunningTimers, mUnpluggables); + mWifiRunningTimers, mOnBatteryTimeBase); mFullWifiLockTimer = new StopwatchTimer(Uid.this, FULL_WIFI_LOCK, - mFullWifiLockTimers, mUnpluggables); + mFullWifiLockTimers, mOnBatteryTimeBase); mWifiScanTimer = new StopwatchTimer(Uid.this, WIFI_SCAN, - mWifiScanTimers, mUnpluggables); + mWifiScanTimers, mOnBatteryTimeBase); mWifiBatchedScanTimer = new StopwatchTimer[NUM_WIFI_BATCHED_SCAN_BINS]; mWifiMulticastTimer = new StopwatchTimer(Uid.this, WIFI_MULTICAST_ENABLED, - mWifiMulticastTimers, mUnpluggables); + mWifiMulticastTimers, mOnBatteryTimeBase); } @Override @@ -2693,67 +3673,67 @@ public final class BatteryStatsImpl extends BatteryStats { } @Override - public void noteWifiRunningLocked() { + public void noteWifiRunningLocked(long elapsedRealtimeMs) { if (!mWifiRunning) { mWifiRunning = true; if (mWifiRunningTimer == null) { mWifiRunningTimer = new StopwatchTimer(Uid.this, WIFI_RUNNING, - mWifiRunningTimers, mUnpluggables); + mWifiRunningTimers, mOnBatteryTimeBase); } - mWifiRunningTimer.startRunningLocked(BatteryStatsImpl.this); + mWifiRunningTimer.startRunningLocked(elapsedRealtimeMs); } } @Override - public void noteWifiStoppedLocked() { + public void noteWifiStoppedLocked(long elapsedRealtimeMs) { if (mWifiRunning) { mWifiRunning = false; - mWifiRunningTimer.stopRunningLocked(BatteryStatsImpl.this); + mWifiRunningTimer.stopRunningLocked(elapsedRealtimeMs); } } @Override - public void noteFullWifiLockAcquiredLocked() { + public void noteFullWifiLockAcquiredLocked(long elapsedRealtimeMs) { if (!mFullWifiLockOut) { mFullWifiLockOut = true; if (mFullWifiLockTimer == null) { mFullWifiLockTimer = new StopwatchTimer(Uid.this, FULL_WIFI_LOCK, - mFullWifiLockTimers, mUnpluggables); + mFullWifiLockTimers, mOnBatteryTimeBase); } - mFullWifiLockTimer.startRunningLocked(BatteryStatsImpl.this); + mFullWifiLockTimer.startRunningLocked(elapsedRealtimeMs); } } @Override - public void noteFullWifiLockReleasedLocked() { + public void noteFullWifiLockReleasedLocked(long elapsedRealtimeMs) { if (mFullWifiLockOut) { mFullWifiLockOut = false; - mFullWifiLockTimer.stopRunningLocked(BatteryStatsImpl.this); + mFullWifiLockTimer.stopRunningLocked(elapsedRealtimeMs); } } @Override - public void noteWifiScanStartedLocked() { + public void noteWifiScanStartedLocked(long elapsedRealtimeMs) { if (!mWifiScanStarted) { mWifiScanStarted = true; if (mWifiScanTimer == null) { mWifiScanTimer = new StopwatchTimer(Uid.this, WIFI_SCAN, - mWifiScanTimers, mUnpluggables); + mWifiScanTimers, mOnBatteryTimeBase); } - mWifiScanTimer.startRunningLocked(BatteryStatsImpl.this); + mWifiScanTimer.startRunningLocked(elapsedRealtimeMs); } } @Override - public void noteWifiScanStoppedLocked() { + public void noteWifiScanStoppedLocked(long elapsedRealtimeMs) { if (mWifiScanStarted) { mWifiScanStarted = false; - mWifiScanTimer.stopRunningLocked(BatteryStatsImpl.this); + mWifiScanTimer.stopRunningLocked(elapsedRealtimeMs); } } @Override - public void noteWifiBatchedScanStartedLocked(int csph) { + public void noteWifiBatchedScanStartedLocked(int csph, long elapsedRealtimeMs) { int bin = 0; while (csph > 8 && bin < NUM_WIFI_BATCHED_SCAN_BINS) { csph = csph >> 3; @@ -2764,66 +3744,66 @@ public final class BatteryStatsImpl extends BatteryStats { if (mWifiBatchedScanBinStarted != NO_BATCHED_SCAN_STARTED) { mWifiBatchedScanTimer[mWifiBatchedScanBinStarted]. - stopRunningLocked(BatteryStatsImpl.this); + stopRunningLocked(elapsedRealtimeMs); } mWifiBatchedScanBinStarted = bin; if (mWifiBatchedScanTimer[bin] == null) { makeWifiBatchedScanBin(bin, null); } - mWifiBatchedScanTimer[bin].startRunningLocked(BatteryStatsImpl.this); + mWifiBatchedScanTimer[bin].startRunningLocked(elapsedRealtimeMs); } @Override - public void noteWifiBatchedScanStoppedLocked() { + public void noteWifiBatchedScanStoppedLocked(long elapsedRealtimeMs) { if (mWifiBatchedScanBinStarted != NO_BATCHED_SCAN_STARTED) { mWifiBatchedScanTimer[mWifiBatchedScanBinStarted]. - stopRunningLocked(BatteryStatsImpl.this); + stopRunningLocked(elapsedRealtimeMs); mWifiBatchedScanBinStarted = NO_BATCHED_SCAN_STARTED; } } @Override - public void noteWifiMulticastEnabledLocked() { + public void noteWifiMulticastEnabledLocked(long elapsedRealtimeMs) { if (!mWifiMulticastEnabled) { mWifiMulticastEnabled = true; if (mWifiMulticastTimer == null) { mWifiMulticastTimer = new StopwatchTimer(Uid.this, WIFI_MULTICAST_ENABLED, - mWifiMulticastTimers, mUnpluggables); + mWifiMulticastTimers, mOnBatteryTimeBase); } - mWifiMulticastTimer.startRunningLocked(BatteryStatsImpl.this); + mWifiMulticastTimer.startRunningLocked(elapsedRealtimeMs); } } @Override - public void noteWifiMulticastDisabledLocked() { + public void noteWifiMulticastDisabledLocked(long elapsedRealtimeMs) { if (mWifiMulticastEnabled) { mWifiMulticastEnabled = false; - mWifiMulticastTimer.stopRunningLocked(BatteryStatsImpl.this); + mWifiMulticastTimer.stopRunningLocked(elapsedRealtimeMs); } } public StopwatchTimer createAudioTurnedOnTimerLocked() { if (mAudioTurnedOnTimer == null) { mAudioTurnedOnTimer = new StopwatchTimer(Uid.this, AUDIO_TURNED_ON, - null, mUnpluggables); + null, mOnBatteryTimeBase); } return mAudioTurnedOnTimer; } @Override - public void noteAudioTurnedOnLocked() { + public void noteAudioTurnedOnLocked(long elapsedRealtimeMs) { if (!mAudioTurnedOn) { mAudioTurnedOn = true; - createAudioTurnedOnTimerLocked().startRunningLocked(BatteryStatsImpl.this); + createAudioTurnedOnTimerLocked().startRunningLocked(elapsedRealtimeMs); } } @Override - public void noteAudioTurnedOffLocked() { + public void noteAudioTurnedOffLocked(long elapsedRealtimeMs) { if (mAudioTurnedOn) { mAudioTurnedOn = false; if (mAudioTurnedOnTimer != null) { - mAudioTurnedOnTimer.stopRunningLocked(BatteryStatsImpl.this); + mAudioTurnedOnTimer.stopRunningLocked(elapsedRealtimeMs); } } } @@ -2831,25 +3811,25 @@ public final class BatteryStatsImpl extends BatteryStats { public StopwatchTimer createVideoTurnedOnTimerLocked() { if (mVideoTurnedOnTimer == null) { mVideoTurnedOnTimer = new StopwatchTimer(Uid.this, VIDEO_TURNED_ON, - null, mUnpluggables); + null, mOnBatteryTimeBase); } return mVideoTurnedOnTimer; } @Override - public void noteVideoTurnedOnLocked() { + public void noteVideoTurnedOnLocked(long elapsedRealtimeMs) { if (!mVideoTurnedOn) { mVideoTurnedOn = true; - createVideoTurnedOnTimerLocked().startRunningLocked(BatteryStatsImpl.this); + createVideoTurnedOnTimerLocked().startRunningLocked(elapsedRealtimeMs); } } @Override - public void noteVideoTurnedOffLocked() { + public void noteVideoTurnedOffLocked(long elapsedRealtimeMs) { if (mVideoTurnedOn) { mVideoTurnedOn = false; if (mVideoTurnedOnTimer != null) { - mVideoTurnedOnTimer.stopRunningLocked(BatteryStatsImpl.this); + mVideoTurnedOnTimer.stopRunningLocked(elapsedRealtimeMs); } } } @@ -2857,28 +3837,27 @@ public final class BatteryStatsImpl extends BatteryStats { public StopwatchTimer createForegroundActivityTimerLocked() { if (mForegroundActivityTimer == null) { mForegroundActivityTimer = new StopwatchTimer( - Uid.this, FOREGROUND_ACTIVITY, null, mUnpluggables); + Uid.this, FOREGROUND_ACTIVITY, null, mOnBatteryTimeBase); } return mForegroundActivityTimer; } @Override - public void noteActivityResumedLocked() { + public void noteActivityResumedLocked(long elapsedRealtimeMs) { // We always start, since we want multiple foreground PIDs to nest - createForegroundActivityTimerLocked().startRunningLocked(BatteryStatsImpl.this); + createForegroundActivityTimerLocked().startRunningLocked(elapsedRealtimeMs); } @Override - public void noteActivityPausedLocked() { + public void noteActivityPausedLocked(long elapsedRealtimeMs) { if (mForegroundActivityTimer != null) { - mForegroundActivityTimer.stopRunningLocked(BatteryStatsImpl.this); + mForegroundActivityTimer.stopRunningLocked(elapsedRealtimeMs); } } public BatchTimer createVibratorOnTimerLocked() { if (mVibratorOnTimer == null) { - mVibratorOnTimer = new BatchTimer(Uid.this, VIBRATOR_ON, - mUnpluggables, BatteryStatsImpl.this.mOnBatteryInternal); + mVibratorOnTimer = new BatchTimer(Uid.this, VIBRATOR_ON, mOnBatteryTimeBase); } return mVibratorOnTimer; } @@ -2894,61 +3873,60 @@ public final class BatteryStatsImpl extends BatteryStats { } @Override - public long getWifiRunningTime(long batteryRealtime, int which) { + public long getWifiRunningTime(long elapsedRealtimeUs, int which) { if (mWifiRunningTimer == null) { return 0; } - return mWifiRunningTimer.getTotalTimeLocked(batteryRealtime, which); + return mWifiRunningTimer.getTotalTimeLocked(elapsedRealtimeUs, which); } @Override - public long getFullWifiLockTime(long batteryRealtime, int which) { + public long getFullWifiLockTime(long elapsedRealtimeUs, int which) { if (mFullWifiLockTimer == null) { return 0; } - return mFullWifiLockTimer.getTotalTimeLocked(batteryRealtime, which); + return mFullWifiLockTimer.getTotalTimeLocked(elapsedRealtimeUs, which); } @Override - public long getWifiScanTime(long batteryRealtime, int which) { + public long getWifiScanTime(long elapsedRealtimeUs, int which) { if (mWifiScanTimer == null) { return 0; } - return mWifiScanTimer.getTotalTimeLocked(batteryRealtime, which); + return mWifiScanTimer.getTotalTimeLocked(elapsedRealtimeUs, which); } @Override - public long getWifiBatchedScanTime(int csphBin, long batteryRealtime, int which) { + public long getWifiBatchedScanTime(int csphBin, long elapsedRealtimeUs, int which) { if (csphBin < 0 || csphBin >= NUM_WIFI_BATCHED_SCAN_BINS) return 0; if (mWifiBatchedScanTimer[csphBin] == null) { return 0; } - return mWifiBatchedScanTimer[csphBin].getTotalTimeLocked(batteryRealtime, which); + return mWifiBatchedScanTimer[csphBin].getTotalTimeLocked(elapsedRealtimeUs, which); } @Override - public long getWifiMulticastTime(long batteryRealtime, int which) { + public long getWifiMulticastTime(long elapsedRealtimeUs, int which) { if (mWifiMulticastTimer == null) { return 0; } - return mWifiMulticastTimer.getTotalTimeLocked(batteryRealtime, - which); + return mWifiMulticastTimer.getTotalTimeLocked(elapsedRealtimeUs, which); } @Override - public long getAudioTurnedOnTime(long batteryRealtime, int which) { + public long getAudioTurnedOnTime(long elapsedRealtimeUs, int which) { if (mAudioTurnedOnTimer == null) { return 0; } - return mAudioTurnedOnTimer.getTotalTimeLocked(batteryRealtime, which); + return mAudioTurnedOnTimer.getTotalTimeLocked(elapsedRealtimeUs, which); } @Override - public long getVideoTurnedOnTime(long batteryRealtime, int which) { + public long getVideoTurnedOnTime(long elapsedRealtimeUs, int which) { if (mVideoTurnedOnTimer == null) { return 0; } - return mVideoTurnedOnTimer.getTotalTimeLocked(batteryRealtime, which); + return mVideoTurnedOnTimer.getTotalTimeLocked(elapsedRealtimeUs, which); } @Override @@ -2997,10 +3975,10 @@ public final class BatteryStatsImpl extends BatteryStats { } if (in == null) { mWifiBatchedScanTimer[i] = new StopwatchTimer(this, WIFI_BATCHED_SCAN, collected, - mUnpluggables); + mOnBatteryTimeBase); } else { mWifiBatchedScanTimer[i] = new StopwatchTimer(this, WIFI_BATCHED_SCAN, collected, - mUnpluggables, in); + mOnBatteryTimeBase, in); } } @@ -3008,42 +3986,77 @@ public final class BatteryStatsImpl extends BatteryStats { void initUserActivityLocked() { mUserActivityCounters = new Counter[NUM_USER_ACTIVITY_TYPES]; for (int i=0; i<NUM_USER_ACTIVITY_TYPES; i++) { - mUserActivityCounters[i] = new Counter(mUnpluggables); + mUserActivityCounters[i] = new Counter(mOnBatteryTimeBase); } } - void noteNetworkActivityLocked(int type, long delta) { - if (mNetworkActivityCounters == null) { + void noteNetworkActivityLocked(int type, long deltaBytes, long deltaPackets) { + if (mNetworkByteActivityCounters == null) { initNetworkActivityLocked(); } if (type >= 0 && type < NUM_NETWORK_ACTIVITY_TYPES) { - mNetworkActivityCounters[type].addCountLocked(delta); + mNetworkByteActivityCounters[type].addCountLocked(deltaBytes); + mNetworkPacketActivityCounters[type].addCountLocked(deltaPackets); } else { Slog.w(TAG, "Unknown network activity type " + type + " was specified.", new Throwable()); } } + void noteMobileRadioActiveTimeLocked(long batteryUptime) { + if (mNetworkByteActivityCounters == null) { + initNetworkActivityLocked(); + } + mMobileRadioActiveTime.addCountLocked(batteryUptime); + mMobileRadioActiveCount.addCountLocked(1); + } + @Override public boolean hasNetworkActivity() { - return mNetworkActivityCounters != null; + return mNetworkByteActivityCounters != null; + } + + @Override + public long getNetworkActivityBytes(int type, int which) { + if (mNetworkByteActivityCounters != null && type >= 0 + && type < mNetworkByteActivityCounters.length) { + return mNetworkByteActivityCounters[type].getCountLocked(which); + } else { + return 0; + } } @Override - public long getNetworkActivityCount(int type, int which) { - if (mNetworkActivityCounters != null && type >= 0 - && type < mNetworkActivityCounters.length) { - return mNetworkActivityCounters[type].getCountLocked(which); + public long getNetworkActivityPackets(int type, int which) { + if (mNetworkPacketActivityCounters != null && type >= 0 + && type < mNetworkPacketActivityCounters.length) { + return mNetworkPacketActivityCounters[type].getCountLocked(which); } else { return 0; } } + @Override + public long getMobileRadioActiveTime(int which) { + return mMobileRadioActiveTime != null + ? mMobileRadioActiveTime.getCountLocked(which) : 0; + } + + @Override + public int getMobileRadioActiveCount(int which) { + return mMobileRadioActiveCount != null + ? (int)mMobileRadioActiveCount.getCountLocked(which) : 0; + } + void initNetworkActivityLocked() { - mNetworkActivityCounters = new LongSamplingCounter[NUM_NETWORK_ACTIVITY_TYPES]; + mNetworkByteActivityCounters = new LongSamplingCounter[NUM_NETWORK_ACTIVITY_TYPES]; + mNetworkPacketActivityCounters = new LongSamplingCounter[NUM_NETWORK_ACTIVITY_TYPES]; for (int i = 0; i < NUM_NETWORK_ACTIVITY_TYPES; i++) { - mNetworkActivityCounters[i] = new LongSamplingCounter(mUnpluggables); + mNetworkByteActivityCounters[i] = new LongSamplingCounter(mOnBatteryTimeBase); + mNetworkPacketActivityCounters[i] = new LongSamplingCounter(mOnBatteryTimeBase); } + mMobileRadioActiveTime = new LongSamplingCounter(mOnBatteryTimeBase); + mMobileRadioActiveCount = new LongSamplingCounter(mOnBatteryTimeBase); } /** @@ -3054,42 +4067,42 @@ public final class BatteryStatsImpl extends BatteryStats { boolean active = false; if (mWifiRunningTimer != null) { - active |= !mWifiRunningTimer.reset(BatteryStatsImpl.this, false); + active |= !mWifiRunningTimer.reset(false); active |= mWifiRunning; } if (mFullWifiLockTimer != null) { - active |= !mFullWifiLockTimer.reset(BatteryStatsImpl.this, false); + active |= !mFullWifiLockTimer.reset(false); active |= mFullWifiLockOut; } if (mWifiScanTimer != null) { - active |= !mWifiScanTimer.reset(BatteryStatsImpl.this, false); + active |= !mWifiScanTimer.reset(false); active |= mWifiScanStarted; } if (mWifiBatchedScanTimer != null) { for (int i = 0; i < NUM_WIFI_BATCHED_SCAN_BINS; i++) { if (mWifiBatchedScanTimer[i] != null) { - active |= !mWifiBatchedScanTimer[i].reset(BatteryStatsImpl.this, false); + active |= !mWifiBatchedScanTimer[i].reset(false); } } active |= (mWifiBatchedScanBinStarted != NO_BATCHED_SCAN_STARTED); } if (mWifiMulticastTimer != null) { - active |= !mWifiMulticastTimer.reset(BatteryStatsImpl.this, false); + active |= !mWifiMulticastTimer.reset(false); active |= mWifiMulticastEnabled; } if (mAudioTurnedOnTimer != null) { - active |= !mAudioTurnedOnTimer.reset(BatteryStatsImpl.this, false); + active |= !mAudioTurnedOnTimer.reset(false); active |= mAudioTurnedOn; } if (mVideoTurnedOnTimer != null) { - active |= !mVideoTurnedOnTimer.reset(BatteryStatsImpl.this, false); + active |= !mVideoTurnedOnTimer.reset(false); active |= mVideoTurnedOn; } if (mForegroundActivityTimer != null) { - active |= !mForegroundActivityTimer.reset(BatteryStatsImpl.this, false); + active |= !mForegroundActivityTimer.reset(false); } if (mVibratorOnTimer != null) { - if (mVibratorOnTimer.reset(BatteryStatsImpl.this, false)) { + if (mVibratorOnTimer.reset(false)) { mVibratorOnTimer.detach(); mVibratorOnTimer = null; } else { @@ -3103,10 +4116,13 @@ public final class BatteryStatsImpl extends BatteryStats { } } - if (mNetworkActivityCounters != null) { + if (mNetworkByteActivityCounters != null) { for (int i = 0; i < NUM_NETWORK_ACTIVITY_TYPES; i++) { - mNetworkActivityCounters[i].reset(false); + mNetworkByteActivityCounters[i].reset(false); + mNetworkPacketActivityCounters[i].reset(false); } + mMobileRadioActiveTime.reset(false); + mMobileRadioActiveCount.reset(false); } if (mWakelockStats.size() > 0) { @@ -3142,10 +4158,12 @@ public final class BatteryStatsImpl extends BatteryStats { mProcessStats.clear(); } if (mPids.size() > 0) { - for (int i=0; !active && i<mPids.size(); i++) { + for (int i=mPids.size()-1; i>=0; i--) { Pid pid = mPids.valueAt(i); - if (pid.mWakeStart != 0) { + if (pid.mWakeNesting > 0) { active = true; + } else { + mPids.removeAt(i); } } } @@ -3167,8 +4185,6 @@ public final class BatteryStatsImpl extends BatteryStats { mPackageStats.clear(); } - mPids.clear(); - if (!active) { if (mWifiRunningTimer != null) { mWifiRunningTimer.detach(); @@ -3204,29 +4220,31 @@ public final class BatteryStatsImpl extends BatteryStats { mUserActivityCounters[i].detach(); } } - if (mNetworkActivityCounters != null) { + if (mNetworkByteActivityCounters != null) { for (int i = 0; i < NUM_NETWORK_ACTIVITY_TYPES; i++) { - mNetworkActivityCounters[i].detach(); + mNetworkByteActivityCounters[i].detach(); + mNetworkPacketActivityCounters[i].detach(); } } + mPids.clear(); } return !active; } - void writeToParcelLocked(Parcel out, long batteryRealtime) { + void writeToParcelLocked(Parcel out, long elapsedRealtimeUs) { out.writeInt(mWakelockStats.size()); for (Map.Entry<String, Uid.Wakelock> wakelockEntry : mWakelockStats.entrySet()) { out.writeString(wakelockEntry.getKey()); Uid.Wakelock wakelock = wakelockEntry.getValue(); - wakelock.writeToParcelLocked(out, batteryRealtime); + wakelock.writeToParcelLocked(out, elapsedRealtimeUs); } out.writeInt(mSensorStats.size()); for (Map.Entry<Integer, Uid.Sensor> sensorEntry : mSensorStats.entrySet()) { out.writeInt(sensorEntry.getKey()); Uid.Sensor sensor = sensorEntry.getValue(); - sensor.writeToParcelLocked(out, batteryRealtime); + sensor.writeToParcelLocked(out, elapsedRealtimeUs); } out.writeInt(mProcessStats.size()); @@ -3245,57 +4263,57 @@ public final class BatteryStatsImpl extends BatteryStats { if (mWifiRunningTimer != null) { out.writeInt(1); - mWifiRunningTimer.writeToParcel(out, batteryRealtime); + mWifiRunningTimer.writeToParcel(out, elapsedRealtimeUs); } else { out.writeInt(0); } if (mFullWifiLockTimer != null) { out.writeInt(1); - mFullWifiLockTimer.writeToParcel(out, batteryRealtime); + mFullWifiLockTimer.writeToParcel(out, elapsedRealtimeUs); } else { out.writeInt(0); } if (mWifiScanTimer != null) { out.writeInt(1); - mWifiScanTimer.writeToParcel(out, batteryRealtime); + mWifiScanTimer.writeToParcel(out, elapsedRealtimeUs); } else { out.writeInt(0); } for (int i = 0; i < NUM_WIFI_BATCHED_SCAN_BINS; i++) { if (mWifiBatchedScanTimer[i] != null) { out.writeInt(1); - mWifiBatchedScanTimer[i].writeToParcel(out, batteryRealtime); + mWifiBatchedScanTimer[i].writeToParcel(out, elapsedRealtimeUs); } else { out.writeInt(0); } } if (mWifiMulticastTimer != null) { out.writeInt(1); - mWifiMulticastTimer.writeToParcel(out, batteryRealtime); + mWifiMulticastTimer.writeToParcel(out, elapsedRealtimeUs); } else { out.writeInt(0); } if (mAudioTurnedOnTimer != null) { out.writeInt(1); - mAudioTurnedOnTimer.writeToParcel(out, batteryRealtime); + mAudioTurnedOnTimer.writeToParcel(out, elapsedRealtimeUs); } else { out.writeInt(0); } if (mVideoTurnedOnTimer != null) { out.writeInt(1); - mVideoTurnedOnTimer.writeToParcel(out, batteryRealtime); + mVideoTurnedOnTimer.writeToParcel(out, elapsedRealtimeUs); } else { out.writeInt(0); } if (mForegroundActivityTimer != null) { out.writeInt(1); - mForegroundActivityTimer.writeToParcel(out, batteryRealtime); + mForegroundActivityTimer.writeToParcel(out, elapsedRealtimeUs); } else { out.writeInt(0); } if (mVibratorOnTimer != null) { out.writeInt(1); - mVibratorOnTimer.writeToParcel(out, batteryRealtime); + mVibratorOnTimer.writeToParcel(out, elapsedRealtimeUs); } else { out.writeInt(0); } @@ -3307,23 +4325,26 @@ public final class BatteryStatsImpl extends BatteryStats { } else { out.writeInt(0); } - if (mNetworkActivityCounters != null) { + if (mNetworkByteActivityCounters != null) { out.writeInt(1); for (int i = 0; i < NUM_NETWORK_ACTIVITY_TYPES; i++) { - mNetworkActivityCounters[i].writeToParcel(out); + mNetworkByteActivityCounters[i].writeToParcel(out); + mNetworkPacketActivityCounters[i].writeToParcel(out); } + mMobileRadioActiveTime.writeToParcel(out); + mMobileRadioActiveCount.writeToParcel(out); } else { out.writeInt(0); } } - void readFromParcelLocked(ArrayList<Unpluggable> unpluggables, Parcel in) { + void readFromParcelLocked(TimeBase timeBase, TimeBase screenOffTimeBase, Parcel in) { int numWakelocks = in.readInt(); mWakelockStats.clear(); for (int j = 0; j < numWakelocks; j++) { String wakelockName = in.readString(); Uid.Wakelock wakelock = new Wakelock(); - wakelock.readFromParcelLocked(unpluggables, in); + wakelock.readFromParcelLocked(timeBase, screenOffTimeBase, in); // We will just drop some random set of wakelocks if // the previous run of the system was an older version // that didn't impose a limit. @@ -3335,7 +4356,7 @@ public final class BatteryStatsImpl extends BatteryStats { for (int k = 0; k < numSensors; k++) { int sensorNumber = in.readInt(); Uid.Sensor sensor = new Sensor(sensorNumber); - sensor.readFromParcelLocked(mUnpluggables, in); + sensor.readFromParcelLocked(mOnBatteryTimeBase, in); mSensorStats.put(sensorNumber, sensor); } @@ -3360,21 +4381,21 @@ public final class BatteryStatsImpl extends BatteryStats { mWifiRunning = false; if (in.readInt() != 0) { mWifiRunningTimer = new StopwatchTimer(Uid.this, WIFI_RUNNING, - mWifiRunningTimers, mUnpluggables, in); + mWifiRunningTimers, mOnBatteryTimeBase, in); } else { mWifiRunningTimer = null; } mFullWifiLockOut = false; if (in.readInt() != 0) { mFullWifiLockTimer = new StopwatchTimer(Uid.this, FULL_WIFI_LOCK, - mFullWifiLockTimers, mUnpluggables, in); + mFullWifiLockTimers, mOnBatteryTimeBase, in); } else { mFullWifiLockTimer = null; } mWifiScanStarted = false; if (in.readInt() != 0) { mWifiScanTimer = new StopwatchTimer(Uid.this, WIFI_SCAN, - mWifiScanTimers, mUnpluggables, in); + mWifiScanTimers, mOnBatteryTimeBase, in); } else { mWifiScanTimer = null; } @@ -3389,51 +4410,58 @@ public final class BatteryStatsImpl extends BatteryStats { mWifiMulticastEnabled = false; if (in.readInt() != 0) { mWifiMulticastTimer = new StopwatchTimer(Uid.this, WIFI_MULTICAST_ENABLED, - mWifiMulticastTimers, mUnpluggables, in); + mWifiMulticastTimers, mOnBatteryTimeBase, in); } else { mWifiMulticastTimer = null; } mAudioTurnedOn = false; if (in.readInt() != 0) { mAudioTurnedOnTimer = new StopwatchTimer(Uid.this, AUDIO_TURNED_ON, - null, mUnpluggables, in); + null, mOnBatteryTimeBase, in); } else { mAudioTurnedOnTimer = null; } mVideoTurnedOn = false; if (in.readInt() != 0) { mVideoTurnedOnTimer = new StopwatchTimer(Uid.this, VIDEO_TURNED_ON, - null, mUnpluggables, in); + null, mOnBatteryTimeBase, in); } else { mVideoTurnedOnTimer = null; } if (in.readInt() != 0) { mForegroundActivityTimer = new StopwatchTimer( - Uid.this, FOREGROUND_ACTIVITY, null, mUnpluggables, in); + Uid.this, FOREGROUND_ACTIVITY, null, mOnBatteryTimeBase, in); } else { mForegroundActivityTimer = null; } if (in.readInt() != 0) { - mVibratorOnTimer = new BatchTimer(Uid.this, VIBRATOR_ON, - mUnpluggables, BatteryStatsImpl.this.mOnBatteryInternal, in); + mVibratorOnTimer = new BatchTimer(Uid.this, VIBRATOR_ON, mOnBatteryTimeBase, in); } else { mVibratorOnTimer = null; } if (in.readInt() != 0) { mUserActivityCounters = new Counter[NUM_USER_ACTIVITY_TYPES]; for (int i=0; i<NUM_USER_ACTIVITY_TYPES; i++) { - mUserActivityCounters[i] = new Counter(mUnpluggables, in); + mUserActivityCounters[i] = new Counter(mOnBatteryTimeBase, in); } } else { mUserActivityCounters = null; } if (in.readInt() != 0) { - mNetworkActivityCounters = new LongSamplingCounter[NUM_NETWORK_ACTIVITY_TYPES]; + mNetworkByteActivityCounters = new LongSamplingCounter[NUM_NETWORK_ACTIVITY_TYPES]; + mNetworkPacketActivityCounters + = new LongSamplingCounter[NUM_NETWORK_ACTIVITY_TYPES]; for (int i = 0; i < NUM_NETWORK_ACTIVITY_TYPES; i++) { - mNetworkActivityCounters[i] = new LongSamplingCounter(mUnpluggables, in); + mNetworkByteActivityCounters[i] + = new LongSamplingCounter(mOnBatteryTimeBase, in); + mNetworkPacketActivityCounters[i] + = new LongSamplingCounter(mOnBatteryTimeBase, in); } + mMobileRadioActiveTime = new LongSamplingCounter(mOnBatteryTimeBase, in); + mMobileRadioActiveCount = new LongSamplingCounter(mOnBatteryTimeBase, in); } else { - mNetworkActivityCounters = null; + mNetworkByteActivityCounters = null; + mNetworkPacketActivityCounters = null; } } @@ -3464,24 +4492,24 @@ public final class BatteryStatsImpl extends BatteryStats { * return a new Timer, or null. */ private StopwatchTimer readTimerFromParcel(int type, ArrayList<StopwatchTimer> pool, - ArrayList<Unpluggable> unpluggables, Parcel in) { + TimeBase timeBase, Parcel in) { if (in.readInt() == 0) { return null; } - return new StopwatchTimer(Uid.this, type, pool, unpluggables, in); + return new StopwatchTimer(Uid.this, type, pool, timeBase, in); } boolean reset() { boolean wlactive = false; if (mTimerFull != null) { - wlactive |= !mTimerFull.reset(BatteryStatsImpl.this, false); + wlactive |= !mTimerFull.reset(false); } if (mTimerPartial != null) { - wlactive |= !mTimerPartial.reset(BatteryStatsImpl.this, false); + wlactive |= !mTimerPartial.reset(false); } if (mTimerWindow != null) { - wlactive |= !mTimerWindow.reset(BatteryStatsImpl.this, false); + wlactive |= !mTimerWindow.reset(false); } if (!wlactive) { if (mTimerFull != null) { @@ -3500,19 +4528,19 @@ public final class BatteryStatsImpl extends BatteryStats { return !wlactive; } - void readFromParcelLocked(ArrayList<Unpluggable> unpluggables, Parcel in) { + void readFromParcelLocked(TimeBase timeBase, TimeBase screenOffTimeBase, Parcel in) { mTimerPartial = readTimerFromParcel(WAKE_TYPE_PARTIAL, - mPartialTimers, unpluggables, in); + mPartialTimers, screenOffTimeBase, in); mTimerFull = readTimerFromParcel(WAKE_TYPE_FULL, - mFullTimers, unpluggables, in); + mFullTimers, timeBase, in); mTimerWindow = readTimerFromParcel(WAKE_TYPE_WINDOW, - mWindowTimers, unpluggables, in); + mWindowTimers, timeBase, in); } - void writeToParcelLocked(Parcel out, long batteryRealtime) { - Timer.writeTimerToParcel(out, mTimerPartial, batteryRealtime); - Timer.writeTimerToParcel(out, mTimerFull, batteryRealtime); - Timer.writeTimerToParcel(out, mTimerWindow, batteryRealtime); + void writeToParcelLocked(Parcel out, long elapsedRealtimeUs) { + Timer.writeTimerToParcel(out, mTimerPartial, elapsedRealtimeUs); + Timer.writeTimerToParcel(out, mTimerFull, elapsedRealtimeUs); + Timer.writeTimerToParcel(out, mTimerWindow, elapsedRealtimeUs); } @Override @@ -3534,8 +4562,7 @@ public final class BatteryStatsImpl extends BatteryStats { mHandle = handle; } - private StopwatchTimer readTimerFromParcel(ArrayList<Unpluggable> unpluggables, - Parcel in) { + private StopwatchTimer readTimerFromParcel(TimeBase timeBase, Parcel in) { if (in.readInt() == 0) { return null; } @@ -3545,23 +4572,23 @@ public final class BatteryStatsImpl extends BatteryStats { pool = new ArrayList<StopwatchTimer>(); mSensorTimers.put(mHandle, pool); } - return new StopwatchTimer(Uid.this, 0, pool, unpluggables, in); + return new StopwatchTimer(Uid.this, 0, pool, timeBase, in); } boolean reset() { - if (mTimer.reset(BatteryStatsImpl.this, true)) { + if (mTimer.reset(true)) { mTimer = null; return true; } return false; } - void readFromParcelLocked(ArrayList<Unpluggable> unpluggables, Parcel in) { - mTimer = readTimerFromParcel(unpluggables, in); + void readFromParcelLocked(TimeBase timeBase, Parcel in) { + mTimer = readTimerFromParcel(timeBase, in); } - void writeToParcelLocked(Parcel out, long batteryRealtime) { - Timer.writeTimerToParcel(out, mTimer, batteryRealtime); + void writeToParcelLocked(Parcel out, long elapsedRealtimeUs) { + Timer.writeTimerToParcel(out, mTimer, elapsedRealtimeUs); } @Override @@ -3578,7 +4605,12 @@ public final class BatteryStatsImpl extends BatteryStats { /** * The statistics associated with a particular process. */ - public final class Proc extends BatteryStats.Uid.Proc implements Unpluggable { + public final class Proc extends BatteryStats.Uid.Proc implements TimeBaseObs { + /** + * Remains true until removed from the stats. + */ + boolean mActive = true; + /** * Total time (in 1/100 sec) spent executing in user code. */ @@ -3664,26 +4696,27 @@ public final class BatteryStatsImpl extends BatteryStats { ArrayList<ExcessivePower> mExcessivePower; Proc() { - mUnpluggables.add(this); + mOnBatteryTimeBase.add(this); mSpeedBins = new SamplingCounter[getCpuSpeedSteps()]; } - public void unplug(long elapsedRealtime, long batteryUptime, long batteryRealtime) { + public void onTimeStarted(long elapsedRealtime, long baseUptime, long baseRealtime) { mUnpluggedUserTime = mUserTime; mUnpluggedSystemTime = mSystemTime; mUnpluggedForegroundTime = mForegroundTime; mUnpluggedStarts = mStarts; } - public void plug(long elapsedRealtime, long batteryUptime, long batteryRealtime) { + public void onTimeStopped(long elapsedRealtime, long baseUptime, long baseRealtime) { } void detach() { - mUnpluggables.remove(this); + mActive = false; + mOnBatteryTimeBase.remove(this); for (int i = 0; i < mSpeedBins.length; i++) { SamplingCounter c = mSpeedBins[i]; if (c != null) { - mUnpluggables.remove(c); + mOnBatteryTimeBase.remove(c); mSpeedBins[i] = null; } } @@ -3812,7 +4845,7 @@ public final class BatteryStatsImpl extends BatteryStats { mSpeedBins = new SamplingCounter[bins >= steps ? bins : steps]; for (int i = 0; i < bins; i++) { if (in.readInt() != 0) { - mSpeedBins[i] = new SamplingCounter(mUnpluggables, in); + mSpeedBins[i] = new SamplingCounter(mOnBatteryTimeBase, in); } } @@ -3837,65 +4870,50 @@ public final class BatteryStatsImpl extends BatteryStats { } @Override + public boolean isActive() { + return mActive; + } + + @Override public long getUserTime(int which) { - long val; - if (which == STATS_LAST) { - val = mLastUserTime; - } else { - val = mUserTime; - if (which == STATS_CURRENT) { - val -= mLoadedUserTime; - } else if (which == STATS_SINCE_UNPLUGGED) { - val -= mUnpluggedUserTime; - } + long val = mUserTime; + if (which == STATS_CURRENT) { + val -= mLoadedUserTime; + } else if (which == STATS_SINCE_UNPLUGGED) { + val -= mUnpluggedUserTime; } return val; } @Override public long getSystemTime(int which) { - long val; - if (which == STATS_LAST) { - val = mLastSystemTime; - } else { - val = mSystemTime; - if (which == STATS_CURRENT) { - val -= mLoadedSystemTime; - } else if (which == STATS_SINCE_UNPLUGGED) { - val -= mUnpluggedSystemTime; - } + long val = mSystemTime; + if (which == STATS_CURRENT) { + val -= mLoadedSystemTime; + } else if (which == STATS_SINCE_UNPLUGGED) { + val -= mUnpluggedSystemTime; } return val; } @Override public long getForegroundTime(int which) { - long val; - if (which == STATS_LAST) { - val = mLastForegroundTime; - } else { - val = mForegroundTime; - if (which == STATS_CURRENT) { - val -= mLoadedForegroundTime; - } else if (which == STATS_SINCE_UNPLUGGED) { - val -= mUnpluggedForegroundTime; - } + long val = mForegroundTime; + if (which == STATS_CURRENT) { + val -= mLoadedForegroundTime; + } else if (which == STATS_SINCE_UNPLUGGED) { + val -= mUnpluggedForegroundTime; } return val; } @Override public int getStarts(int which) { - int val; - if (which == STATS_LAST) { - val = mLastStarts; - } else { - val = mStarts; - if (which == STATS_CURRENT) { - val -= mLoadedStarts; - } else if (which == STATS_SINCE_UNPLUGGED) { - val -= mUnpluggedStarts; - } + int val = mStarts; + if (which == STATS_CURRENT) { + val -= mLoadedStarts; + } else if (which == STATS_SINCE_UNPLUGGED) { + val -= mUnpluggedStarts; } return val; } @@ -3907,7 +4925,7 @@ public final class BatteryStatsImpl extends BatteryStats { if (amt != 0) { SamplingCounter c = mSpeedBins[i]; if (c == null) { - mSpeedBins[i] = c = new SamplingCounter(mUnpluggables); + mSpeedBins[i] = c = new SamplingCounter(mOnBatteryTimeBase); } c.addCountAtomic(values[i]); } @@ -3928,7 +4946,7 @@ public final class BatteryStatsImpl extends BatteryStats { /** * The statistics associated with a particular package. */ - public final class Pkg extends BatteryStats.Uid.Pkg implements Unpluggable { + public final class Pkg extends BatteryStats.Uid.Pkg implements TimeBaseObs { /** * Number of times this package has done something that could wake up the * device from sleep. @@ -3959,18 +4977,18 @@ public final class BatteryStatsImpl extends BatteryStats { final HashMap<String, Serv> mServiceStats = new HashMap<String, Serv>(); Pkg() { - mUnpluggables.add(this); + mOnBatteryScreenOffTimeBase.add(this); } - public void unplug(long elapsedRealtime, long batteryUptime, long batteryRealtime) { + public void onTimeStarted(long elapsedRealtime, long baseUptime, long baseRealtime) { mUnpluggedWakeups = mWakeups; } - public void plug(long elapsedRealtime, long batteryUptime, long batteryRealtime) { + public void onTimeStopped(long elapsedRealtime, long baseUptime, long baseRealtime) { } void detach() { - mUnpluggables.remove(this); + mOnBatteryScreenOffTimeBase.remove(this); } void readFromParcelLocked(Parcel in) { @@ -4011,16 +5029,11 @@ public final class BatteryStatsImpl extends BatteryStats { @Override public int getWakeups(int which) { - int val; - if (which == STATS_LAST) { - val = mLastWakeups; - } else { - val = mWakeups; - if (which == STATS_CURRENT) { - val -= mLoadedWakeups; - } else if (which == STATS_SINCE_UNPLUGGED) { - val -= mUnpluggedWakeups; - } + int val = mWakeups; + if (which == STATS_CURRENT) { + val -= mLoadedWakeups; + } else if (which == STATS_SINCE_UNPLUGGED) { + val -= mUnpluggedWakeups; } return val; @@ -4029,7 +5042,7 @@ public final class BatteryStatsImpl extends BatteryStats { /** * The statistics associated with a particular service. */ - public final class Serv extends BatteryStats.Uid.Pkg.Serv implements Unpluggable { + public final class Serv extends BatteryStats.Uid.Pkg.Serv implements TimeBaseObs { /** * Total time (ms in battery uptime) the service has been left started. */ @@ -4121,20 +5134,22 @@ public final class BatteryStatsImpl extends BatteryStats { int mUnpluggedLaunches; Serv() { - mUnpluggables.add(this); + mOnBatteryTimeBase.add(this); } - public void unplug(long elapsedRealtime, long batteryUptime, long batteryRealtime) { - mUnpluggedStartTime = getStartTimeToNowLocked(batteryUptime); + public void onTimeStarted(long elapsedRealtime, long baseUptime, + long baseRealtime) { + mUnpluggedStartTime = getStartTimeToNowLocked(baseUptime); mUnpluggedStarts = mStarts; mUnpluggedLaunches = mLaunches; } - public void plug(long elapsedRealtime, long batteryUptime, long batteryRealtime) { + public void onTimeStopped(long elapsedRealtime, long baseUptime, + long baseRealtime) { } void detach() { - mUnpluggables.remove(this); + mOnBatteryTimeBase.remove(this); } void readFromParcelLocked(Parcel in) { @@ -4230,51 +5245,33 @@ public final class BatteryStatsImpl extends BatteryStats { @Override public int getLaunches(int which) { - int val; - - if (which == STATS_LAST) { - val = mLastLaunches; - } else { - val = mLaunches; - if (which == STATS_CURRENT) { - val -= mLoadedLaunches; - } else if (which == STATS_SINCE_UNPLUGGED) { - val -= mUnpluggedLaunches; - } + int val = mLaunches; + if (which == STATS_CURRENT) { + val -= mLoadedLaunches; + } else if (which == STATS_SINCE_UNPLUGGED) { + val -= mUnpluggedLaunches; } - return val; } @Override public long getStartTime(long now, int which) { - long val; - if (which == STATS_LAST) { - val = mLastStartTime; - } else { - val = getStartTimeToNowLocked(now); - if (which == STATS_CURRENT) { - val -= mLoadedStartTime; - } else if (which == STATS_SINCE_UNPLUGGED) { - val -= mUnpluggedStartTime; - } + long val = getStartTimeToNowLocked(now); + if (which == STATS_CURRENT) { + val -= mLoadedStartTime; + } else if (which == STATS_SINCE_UNPLUGGED) { + val -= mUnpluggedStartTime; } - return val; } @Override public int getStarts(int which) { - int val; - if (which == STATS_LAST) { - val = mLastStarts; - } else { - val = mStarts; - if (which == STATS_CURRENT) { - val -= mLoadedStarts; - } else if (which == STATS_SINCE_UNPLUGGED) { - val -= mUnpluggedStarts; - } + int val = mStarts; + if (which == STATS_CURRENT) { + val -= mLoadedStarts; + } else if (which == STATS_SINCE_UNPLUGGED) { + val -= mUnpluggedStarts; } return val; @@ -4369,7 +5366,7 @@ public final class BatteryStatsImpl extends BatteryStats { t = wl.mTimerPartial; if (t == null) { t = new StopwatchTimer(Uid.this, WAKE_TYPE_PARTIAL, - mPartialTimers, mUnpluggables); + mPartialTimers, mOnBatteryScreenOffTimeBase); wl.mTimerPartial = t; } return t; @@ -4377,7 +5374,7 @@ public final class BatteryStatsImpl extends BatteryStats { t = wl.mTimerFull; if (t == null) { t = new StopwatchTimer(Uid.this, WAKE_TYPE_FULL, - mFullTimers, mUnpluggables); + mFullTimers, mOnBatteryTimeBase); wl.mTimerFull = t; } return t; @@ -4385,7 +5382,7 @@ public final class BatteryStatsImpl extends BatteryStats { t = wl.mTimerWindow; if (t == null) { t = new StopwatchTimer(Uid.this, WAKE_TYPE_WINDOW, - mWindowTimers, mUnpluggables); + mWindowTimers, mOnBatteryTimeBase); wl.mTimerWindow = t; } return t; @@ -4412,34 +5409,36 @@ public final class BatteryStatsImpl extends BatteryStats { timers = new ArrayList<StopwatchTimer>(); mSensorTimers.put(sensor, timers); } - t = new StopwatchTimer(Uid.this, BatteryStats.SENSOR, timers, mUnpluggables); + t = new StopwatchTimer(Uid.this, BatteryStats.SENSOR, timers, mOnBatteryTimeBase); se.mTimer = t; return t; } - public void noteStartWakeLocked(int pid, String name, int type) { + public void noteStartWakeLocked(int pid, String name, int type, long elapsedRealtimeMs) { StopwatchTimer t = getWakeTimerLocked(name, type); if (t != null) { - t.startRunningLocked(BatteryStatsImpl.this); + t.startRunningLocked(elapsedRealtimeMs); } if (pid >= 0 && type == WAKE_TYPE_PARTIAL) { Pid p = getPidStatsLocked(pid); - if (p.mWakeStart == 0) { - p.mWakeStart = SystemClock.elapsedRealtime(); + if (p.mWakeNesting++ == 0) { + p.mWakeStartMs = elapsedRealtimeMs; } } } - public void noteStopWakeLocked(int pid, String name, int type) { + public void noteStopWakeLocked(int pid, String name, int type, long elapsedRealtimeMs) { StopwatchTimer t = getWakeTimerLocked(name, type); if (t != null) { - t.stopRunningLocked(BatteryStatsImpl.this); + t.stopRunningLocked(elapsedRealtimeMs); } if (pid >= 0 && type == WAKE_TYPE_PARTIAL) { Pid p = mPids.get(pid); - if (p != null && p.mWakeStart != 0) { - p.mWakeSum += SystemClock.elapsedRealtime() - p.mWakeStart; - p.mWakeStart = 0; + if (p != null && p.mWakeNesting > 0) { + if (p.mWakeNesting-- == 1) { + p.mWakeSumMs += elapsedRealtimeMs - p.mWakeStartMs; + p.mWakeStartMs = 0; + } } } } @@ -4458,32 +5457,32 @@ public final class BatteryStatsImpl extends BatteryStats { } } - public void noteStartSensor(int sensor) { + public void noteStartSensor(int sensor, long elapsedRealtimeMs) { StopwatchTimer t = getSensorTimerLocked(sensor, true); if (t != null) { - t.startRunningLocked(BatteryStatsImpl.this); + t.startRunningLocked(elapsedRealtimeMs); } } - public void noteStopSensor(int sensor) { + public void noteStopSensor(int sensor, long elapsedRealtimeMs) { // Don't create a timer if one doesn't already exist StopwatchTimer t = getSensorTimerLocked(sensor, false); if (t != null) { - t.stopRunningLocked(BatteryStatsImpl.this); + t.stopRunningLocked(elapsedRealtimeMs); } } - public void noteStartGps() { + public void noteStartGps(long elapsedRealtimeMs) { StopwatchTimer t = getSensorTimerLocked(Sensor.GPS, true); if (t != null) { - t.startRunningLocked(BatteryStatsImpl.this); + t.startRunningLocked(elapsedRealtimeMs); } } - public void noteStopGps() { + public void noteStopGps(long elapsedRealtimeMs) { StopwatchTimer t = getSensorTimerLocked(Sensor.GPS, false); if (t != null) { - t.stopRunningLocked(BatteryStatsImpl.this); + t.stopRunningLocked(elapsedRealtimeMs); } } @@ -4496,38 +5495,50 @@ public final class BatteryStatsImpl extends BatteryStats { mFile = new JournaledFile(new File(filename), new File(filename + ".tmp")); mHandler = new MyHandler(handler.getLooper()); mStartCount++; - mScreenOnTimer = new StopwatchTimer(null, -1, null, mUnpluggables); + mScreenOnTimer = new StopwatchTimer(null, -1, null, mOnBatteryTimeBase); for (int i=0; i<NUM_SCREEN_BRIGHTNESS_BINS; i++) { - mScreenBrightnessTimer[i] = new StopwatchTimer(null, -100-i, null, mUnpluggables); + mScreenBrightnessTimer[i] = new StopwatchTimer(null, -100-i, null, mOnBatteryTimeBase); } - mInputEventCounter = new Counter(mUnpluggables); - mPhoneOnTimer = new StopwatchTimer(null, -2, null, mUnpluggables); + mInputEventCounter = new Counter(mOnBatteryTimeBase); + mPhoneOnTimer = new StopwatchTimer(null, -2, null, mOnBatteryTimeBase); for (int i=0; i<SignalStrength.NUM_SIGNAL_STRENGTH_BINS; i++) { - mPhoneSignalStrengthsTimer[i] = new StopwatchTimer(null, -200-i, null, mUnpluggables); + mPhoneSignalStrengthsTimer[i] = new StopwatchTimer(null, -200-i, null, + mOnBatteryTimeBase); } - mPhoneSignalScanningTimer = new StopwatchTimer(null, -200+1, null, mUnpluggables); + mPhoneSignalScanningTimer = new StopwatchTimer(null, -200+1, null, mOnBatteryTimeBase); for (int i=0; i<NUM_DATA_CONNECTION_TYPES; i++) { - mPhoneDataConnectionsTimer[i] = new StopwatchTimer(null, -300-i, null, mUnpluggables); + mPhoneDataConnectionsTimer[i] = new StopwatchTimer(null, -300-i, null, + mOnBatteryTimeBase); } for (int i = 0; i < NUM_NETWORK_ACTIVITY_TYPES; i++) { - mNetworkActivityCounters[i] = new LongSamplingCounter(mUnpluggables); - } - mWifiOnTimer = new StopwatchTimer(null, -3, null, mUnpluggables); - mGlobalWifiRunningTimer = new StopwatchTimer(null, -4, null, mUnpluggables); - mBluetoothOnTimer = new StopwatchTimer(null, -5, null, mUnpluggables); - mAudioOnTimer = new StopwatchTimer(null, -6, null, mUnpluggables); - mVideoOnTimer = new StopwatchTimer(null, -7, null, mUnpluggables); + mNetworkByteActivityCounters[i] = new LongSamplingCounter(mOnBatteryTimeBase); + mNetworkPacketActivityCounters[i] = new LongSamplingCounter(mOnBatteryTimeBase); + } + mMobileRadioActiveTimer = new StopwatchTimer(null, -400, null, mOnBatteryTimeBase); + mMobileRadioActivePerAppTimer = new StopwatchTimer(null, -401, null, mOnBatteryTimeBase); + mMobileRadioActiveAdjustedTime = new LongSamplingCounter(mOnBatteryTimeBase); + mMobileRadioActiveUnknownTime = new LongSamplingCounter(mOnBatteryTimeBase); + mMobileRadioActiveUnknownCount = new LongSamplingCounter(mOnBatteryTimeBase); + mWifiOnTimer = new StopwatchTimer(null, -3, null, mOnBatteryTimeBase); + mGlobalWifiRunningTimer = new StopwatchTimer(null, -4, null, mOnBatteryTimeBase); + for (int i=0; i<NUM_WIFI_STATES; i++) { + mWifiStateTimer[i] = new StopwatchTimer(null, -600-i, null, mOnBatteryTimeBase); + } + mBluetoothOnTimer = new StopwatchTimer(null, -5, null, mOnBatteryTimeBase); + for (int i=0; i< NUM_BLUETOOTH_STATES; i++) { + mBluetoothStateTimer[i] = new StopwatchTimer(null, -500-i, null, mOnBatteryTimeBase); + } + mAudioOnTimer = new StopwatchTimer(null, -6, null, mOnBatteryTimeBase); + mVideoOnTimer = new StopwatchTimer(null, -7, null, mOnBatteryTimeBase); mOnBattery = mOnBatteryInternal = false; - initTimes(); - mTrackBatteryPastUptime = 0; - mTrackBatteryPastRealtime = 0; - mUptimeStart = mTrackBatteryUptimeStart = SystemClock.uptimeMillis() * 1000; - mRealtimeStart = mTrackBatteryRealtimeStart = SystemClock.elapsedRealtime() * 1000; - mUnpluggedBatteryUptime = getBatteryUptimeLocked(mUptimeStart); - mUnpluggedBatteryRealtime = getBatteryRealtimeLocked(mRealtimeStart); + long uptime = SystemClock.uptimeMillis() * 1000; + long realtime = SystemClock.elapsedRealtime() * 1000; + initTimes(uptime, realtime); mDischargeStartLevel = 0; mDischargeUnplugLevel = 0; + mDischargePlugLevel = -1; mDischargeCurrentLevel = 0; + mCurrentBatteryLevel = 0; initDischarge(); clearHistoryLocked(); } @@ -4557,18 +5568,21 @@ public final class BatteryStatsImpl extends BatteryStats { public boolean startIteratingOldHistoryLocked() { if (DEBUG_HISTORY) Slog.i(TAG, "ITERATING: buff size=" + mHistoryBuffer.dataSize() + " pos=" + mHistoryBuffer.dataPosition()); + if ((mHistoryIterator = mHistory) == null) { + return false; + } mHistoryBuffer.setDataPosition(0); mHistoryReadTmp.clear(); mReadOverflow = false; mIteratingHistory = true; - return (mHistoryIterator = mHistory) != null; + return true; } @Override public boolean getNextOldHistoryLocked(HistoryItem out) { boolean end = mHistoryBuffer.dataPosition() >= mHistoryBuffer.dataSize(); if (!end) { - mHistoryReadTmp.readDelta(mHistoryBuffer); + readHistoryDelta(mHistoryBuffer, mHistoryReadTmp); mReadOverflow |= mHistoryReadTmp.cmd == HistoryItem.CMD_OVERFLOW; } HistoryItem cur = mHistoryIterator; @@ -4584,13 +5598,13 @@ public final class BatteryStatsImpl extends BatteryStats { if (end) { Slog.w(TAG, "New history ends before old history!"); } else if (!out.same(mHistoryReadTmp)) { - long now = getHistoryBaseTime() + SystemClock.elapsedRealtime(); PrintWriter pw = new FastPrintWriter(new LogWriter(android.util.Log.WARN, TAG)); pw.println("Histories differ!"); pw.println("Old history:"); - (new HistoryPrinter()).printNextItem(pw, out, now); + (new HistoryPrinter()).printNextItem(pw, out, 0, false, true); pw.println("New history:"); - (new HistoryPrinter()).printNextItem(pw, mHistoryReadTmp, now); + (new HistoryPrinter()).printNextItem(pw, mHistoryReadTmp, 0, false, + true); pw.flush(); } } @@ -4601,16 +5615,60 @@ public final class BatteryStatsImpl extends BatteryStats { public void finishIteratingOldHistoryLocked() { mIteratingHistory = false; mHistoryBuffer.setDataPosition(mHistoryBuffer.dataSize()); + mHistoryIterator = null; + } + + public int getHistoryTotalSize() { + return MAX_HISTORY_BUFFER; + } + + public int getHistoryUsedSize() { + return mHistoryBuffer.dataSize(); } @Override public boolean startIteratingHistoryLocked() { if (DEBUG_HISTORY) Slog.i(TAG, "ITERATING: buff size=" + mHistoryBuffer.dataSize() + " pos=" + mHistoryBuffer.dataPosition()); + if (mHistoryBuffer.dataSize() <= 0) { + return false; + } mHistoryBuffer.setDataPosition(0); mReadOverflow = false; mIteratingHistory = true; - return mHistoryBuffer.dataSize() > 0; + mReadHistoryStrings = new String[mHistoryTagPool.size()]; + mReadHistoryUids = new int[mHistoryTagPool.size()]; + mReadHistoryChars = 0; + for (HashMap.Entry<HistoryTag, Integer> ent : mHistoryTagPool.entrySet()) { + final HistoryTag tag = ent.getKey(); + final int idx = ent.getValue(); + mReadHistoryStrings[idx] = tag.string; + mReadHistoryUids[idx] = tag.uid; + mReadHistoryChars += tag.string.length() + 1; + } + return true; + } + + @Override + public int getHistoryStringPoolSize() { + return mReadHistoryStrings.length; + } + + @Override + public int getHistoryStringPoolBytes() { + // Each entry is a fixed 12 bytes: 4 for index, 4 for uid, 4 for string size + // Each string character is 2 bytes. + return (mReadHistoryStrings.length * 12) + (mReadHistoryChars * 2); + } + + @Override + public String getHistoryTagPoolString(int index) { + return mReadHistoryStrings[index]; + } + + @Override + public int getHistoryTagPoolUid(int index) { + return mReadHistoryUids[index]; } @Override @@ -4624,7 +5682,12 @@ public final class BatteryStatsImpl extends BatteryStats { return false; } - out.readDelta(mHistoryBuffer); + final long lastRealtime = out.time; + final long lastWalltime = out.currentTime; + readHistoryDelta(mHistoryBuffer, out); + if (out.cmd != HistoryItem.CMD_CURRENT_TIME && lastWalltime != 0) { + out.currentTime = lastWalltime + (out.time - lastRealtime); + } return true; } @@ -4632,6 +5695,7 @@ public final class BatteryStatsImpl extends BatteryStats { public void finishIteratingHistoryLocked() { mIteratingHistory = false; mHistoryBuffer.setDataPosition(mHistoryBuffer.dataSize()); + mReadHistoryStrings = null; } @Override @@ -4652,13 +5716,14 @@ public final class BatteryStatsImpl extends BatteryStats { return mScreenOn; } - void initTimes() { - mBatteryRealtime = mTrackBatteryPastUptime = 0; - mBatteryUptime = mTrackBatteryPastRealtime = 0; - mUptimeStart = mTrackBatteryUptimeStart = SystemClock.uptimeMillis() * 1000; - mRealtimeStart = mTrackBatteryRealtimeStart = SystemClock.elapsedRealtime() * 1000; - mUnpluggedBatteryUptime = getBatteryUptimeLocked(mUptimeStart); - mUnpluggedBatteryRealtime = getBatteryRealtimeLocked(mRealtimeStart); + void initTimes(long uptime, long realtime) { + mStartClockTime = System.currentTimeMillis(); + mOnBatteryTimeBase.init(uptime, realtime); + mOnBatteryScreenOffTimeBase.init(uptime, realtime); + mRealtime = 0; + mUptime = 0; + mRealtimeStart = realtime; + mUptimeStart = uptime; } void initDischarge() { @@ -4668,32 +5733,75 @@ public final class BatteryStatsImpl extends BatteryStats { mDischargeAmountScreenOnSinceCharge = 0; mDischargeAmountScreenOff = 0; mDischargeAmountScreenOffSinceCharge = 0; + mLastDischargeStepTime = -1; + mNumDischargeStepDurations = 0; + mLastChargeStepTime = -1; + mNumChargeStepDurations = 0; } - - public void resetAllStatsLocked() { + + public void resetAllStatsCmdLocked() { + resetAllStatsLocked(); + final long mSecUptime = SystemClock.uptimeMillis(); + long uptime = mSecUptime * 1000; + long mSecRealtime = SystemClock.elapsedRealtime(); + long realtime = mSecRealtime * 1000; + mDischargeStartLevel = mHistoryCur.batteryLevel; + pullPendingStateUpdatesLocked(); + addHistoryRecordLocked(mSecRealtime, mSecUptime); + mDischargeCurrentLevel = mDischargeUnplugLevel = mDischargePlugLevel + = mCurrentBatteryLevel = mHistoryCur.batteryLevel; + mOnBatteryTimeBase.reset(uptime, realtime); + mOnBatteryScreenOffTimeBase.reset(uptime, realtime); + if ((mHistoryCur.states&HistoryItem.STATE_BATTERY_PLUGGED_FLAG) == 0) { + if (mScreenOn) { + mDischargeScreenOnUnplugLevel = mHistoryCur.batteryLevel; + mDischargeScreenOffUnplugLevel = 0; + } else { + mDischargeScreenOnUnplugLevel = 0; + mDischargeScreenOffUnplugLevel = mHistoryCur.batteryLevel; + } + mDischargeAmountScreenOn = 0; + mDischargeAmountScreenOff = 0; + } + initActiveHistoryEventsLocked(mSecRealtime, mSecUptime); + } + + private void resetAllStatsLocked() { mStartCount = 0; - initTimes(); - mScreenOnTimer.reset(this, false); + initTimes(SystemClock.uptimeMillis() * 1000, SystemClock.elapsedRealtime() * 1000); + mScreenOnTimer.reset(false); for (int i=0; i<NUM_SCREEN_BRIGHTNESS_BINS; i++) { - mScreenBrightnessTimer[i].reset(this, false); + mScreenBrightnessTimer[i].reset(false); } mInputEventCounter.reset(false); - mPhoneOnTimer.reset(this, false); - mAudioOnTimer.reset(this, false); - mVideoOnTimer.reset(this, false); + mPhoneOnTimer.reset(false); + mAudioOnTimer.reset(false); + mVideoOnTimer.reset(false); for (int i=0; i<SignalStrength.NUM_SIGNAL_STRENGTH_BINS; i++) { - mPhoneSignalStrengthsTimer[i].reset(this, false); + mPhoneSignalStrengthsTimer[i].reset(false); } - mPhoneSignalScanningTimer.reset(this, false); + mPhoneSignalScanningTimer.reset(false); for (int i=0; i<NUM_DATA_CONNECTION_TYPES; i++) { - mPhoneDataConnectionsTimer[i].reset(this, false); + mPhoneDataConnectionsTimer[i].reset(false); } for (int i = 0; i < NUM_NETWORK_ACTIVITY_TYPES; i++) { - mNetworkActivityCounters[i].reset(false); + mNetworkByteActivityCounters[i].reset(false); + mNetworkPacketActivityCounters[i].reset(false); + } + mMobileRadioActiveTimer.reset(false); + mMobileRadioActivePerAppTimer.reset(false); + mMobileRadioActiveAdjustedTime.reset(false); + mMobileRadioActiveUnknownTime.reset(false); + mMobileRadioActiveUnknownCount.reset(false); + mWifiOnTimer.reset(false); + mGlobalWifiRunningTimer.reset(false); + for (int i=0; i<NUM_WIFI_STATES; i++) { + mWifiStateTimer[i].reset(false); + } + mBluetoothOnTimer.reset(false); + for (int i=0; i< NUM_BLUETOOTH_STATES; i++) { + mBluetoothStateTimer[i].reset(false); } - mWifiOnTimer.reset(this, false); - mGlobalWifiRunningTimer.reset(this, false); - mBluetoothOnTimer.reset(this, false); for (int i=0; i<mUidStats.size(); i++) { if (mUidStats.valueAt(i).reset()) { @@ -4704,16 +5812,41 @@ public final class BatteryStatsImpl extends BatteryStats { if (mKernelWakelockStats.size() > 0) { for (SamplingTimer timer : mKernelWakelockStats.values()) { - mUnpluggables.remove(timer); + mOnBatteryScreenOffTimeBase.remove(timer); } mKernelWakelockStats.clear(); } - + + if (mWakeupReasonStats.size() > 0) { + for (LongSamplingCounter timer : mWakeupReasonStats.values()) { + mOnBatteryScreenOffTimeBase.remove(timer); + } + mWakeupReasonStats.clear(); + } + initDischarge(); clearHistoryLocked(); } + private void initActiveHistoryEventsLocked(long elapsedRealtimeMs, long uptimeMs) { + for (int i=0; i<HistoryItem.EVENT_COUNT; i++) { + HashMap<String, SparseBooleanArray> active = mActiveEvents[i]; + if (active == null) { + continue; + } + for (HashMap.Entry<String, SparseBooleanArray> ent : active.entrySet()) { + SparseBooleanArray uids = ent.getValue(); + for (int j=0; j<uids.size(); j++) { + if (uids.valueAt(j)) { + addHistoryEventLocked(elapsedRealtimeMs, uptimeMs, i, ent.getKey(), + uids.keyAt(j)); + } + } + } + } + } + void updateDischargeScreenLevelsLocked(boolean oldScreenOn, boolean newScreenOn) { if (oldScreenOn) { int diff = mDischargeScreenOnUnplugLevel - mDischargeCurrentLevel; @@ -4737,45 +5870,51 @@ public final class BatteryStatsImpl extends BatteryStats { } } - void setOnBattery(boolean onBattery, int oldStatus, int level) { - synchronized(this) { - setOnBatteryLocked(onBattery, oldStatus, level); + public void pullPendingStateUpdatesLocked() { + updateKernelWakelocksLocked(); + updateNetworkActivityLocked(NET_UPDATE_ALL, SystemClock.elapsedRealtime()); + if (mOnBatteryInternal) { + updateDischargeScreenLevelsLocked(mScreenOn, mScreenOn); } } - void setOnBatteryLocked(boolean onBattery, int oldStatus, int level) { + void setOnBatteryLocked(final long mSecRealtime, final long mSecUptime, final boolean onBattery, + final int oldStatus, final int level) { boolean doWrite = false; Message m = mHandler.obtainMessage(MSG_REPORT_POWER_CHANGE); m.arg1 = onBattery ? 1 : 0; mHandler.sendMessage(m); mOnBattery = mOnBatteryInternal = onBattery; - long uptime = SystemClock.uptimeMillis() * 1000; - long mSecRealtime = SystemClock.elapsedRealtime(); - long realtime = mSecRealtime * 1000; + final long uptime = mSecUptime * 1000; + final long realtime = mSecRealtime * 1000; if (onBattery) { // We will reset our status if we are unplugging after the // battery was last full, or the level is at 100, or // we have gone through a significant charge (from a very low // level to a now very high level). + boolean reset = false; if (oldStatus == BatteryManager.BATTERY_STATUS_FULL || level >= 90 || (mDischargeCurrentLevel < 20 && level >= 80)) { doWrite = true; resetAllStatsLocked(); mDischargeStartLevel = level; + reset = true; + mNumDischargeStepDurations = 0; } - updateKernelWakelocksLocked(); - updateNetworkActivityLocked(); + mLastDischargeStepLevel = level; + mLastDischargeStepTime = -1; + pullPendingStateUpdatesLocked(); mHistoryCur.batteryLevel = (byte)level; mHistoryCur.states &= ~HistoryItem.STATE_BATTERY_PLUGGED_FLAG; if (DEBUG_HISTORY) Slog.v(TAG, "Battery unplugged to: " + Integer.toHexString(mHistoryCur.states)); - addHistoryRecordLocked(mSecRealtime); - mTrackBatteryUptimeStart = uptime; - mTrackBatteryRealtimeStart = realtime; - mUnpluggedBatteryUptime = getBatteryUptimeLocked(uptime); - mUnpluggedBatteryRealtime = getBatteryRealtimeLocked(realtime); + if (reset) { + mRecordingHistory = true; + startRecordingHistory(mSecRealtime, mSecUptime, reset); + } + addHistoryRecordLocked(mSecRealtime, mSecUptime); mDischargeCurrentLevel = mDischargeUnplugLevel = level; if (mScreenOn) { mDischargeScreenOnUnplugLevel = level; @@ -4786,24 +5925,24 @@ public final class BatteryStatsImpl extends BatteryStats { } mDischargeAmountScreenOn = 0; mDischargeAmountScreenOff = 0; - doUnplugLocked(realtime, mUnpluggedBatteryUptime, mUnpluggedBatteryRealtime); + updateTimeBasesLocked(true, !mScreenOn, uptime, realtime); } else { - updateKernelWakelocksLocked(); - updateNetworkActivityLocked(); + pullPendingStateUpdatesLocked(); mHistoryCur.batteryLevel = (byte)level; mHistoryCur.states |= HistoryItem.STATE_BATTERY_PLUGGED_FLAG; if (DEBUG_HISTORY) Slog.v(TAG, "Battery plugged to: " + Integer.toHexString(mHistoryCur.states)); - addHistoryRecordLocked(mSecRealtime); - mTrackBatteryPastUptime += uptime - mTrackBatteryUptimeStart; - mTrackBatteryPastRealtime += realtime - mTrackBatteryRealtimeStart; - mDischargeCurrentLevel = level; + addHistoryRecordLocked(mSecRealtime, mSecUptime); + mDischargeCurrentLevel = mDischargePlugLevel = level; if (level < mDischargeUnplugLevel) { mLowDischargeAmountSinceCharge += mDischargeUnplugLevel-level-1; mHighDischargeAmountSinceCharge += mDischargeUnplugLevel-level; } updateDischargeScreenLevelsLocked(mScreenOn, mScreenOn); - doPlugLocked(realtime, getBatteryUptimeLocked(uptime), getBatteryRealtimeLocked(realtime)); + updateTimeBasesLocked(false, !mScreenOn, uptime, realtime); + mNumChargeStepDurations = 0; + mLastChargeStepLevel = level; + mLastChargeStepTime = -1; } if (doWrite || (mLastWriteTime + (60 * 1000)) < mSecRealtime) { if (mFile != null) { @@ -4812,13 +5951,45 @@ public final class BatteryStatsImpl extends BatteryStats { } } + private void startRecordingHistory(final long elapsedRealtimeMs, final long uptimeMs, + boolean reset) { + mRecordingHistory = true; + mHistoryCur.currentTime = System.currentTimeMillis(); + addHistoryBufferLocked(elapsedRealtimeMs, uptimeMs, HistoryItem.CMD_CURRENT_TIME, + mHistoryCur); + mHistoryCur.currentTime = 0; + if (reset) { + initActiveHistoryEventsLocked(elapsedRealtimeMs, uptimeMs); + } + } + // This should probably be exposed in the API, though it's not critical private static final int BATTERY_PLUGGED_NONE = 0; + private static int addLevelSteps(long[] steps, int stepCount, long lastStepTime, + int numStepLevels, long elapsedRealtime) { + if (lastStepTime >= 0 && numStepLevels > 0) { + long duration = elapsedRealtime - lastStepTime; + for (int i=0; i<numStepLevels; i++) { + System.arraycopy(steps, 0, steps, 1, steps.length-1); + long thisDuration = duration / (numStepLevels-i); + duration -= thisDuration; + steps[0] = thisDuration; + } + stepCount += numStepLevels; + if (stepCount > steps.length) { + stepCount = steps.length; + } + } + return stepCount; + } + public void setBatteryState(int status, int health, int plugType, int level, int temp, int volt) { synchronized(this) { - boolean onBattery = plugType == BATTERY_PLUGGED_NONE; + final boolean onBattery = plugType == BATTERY_PLUGGED_NONE; + final long uptime = SystemClock.uptimeMillis(); + final long elapsedRealtime = SystemClock.elapsedRealtime(); int oldStatus = mHistoryCur.batteryStatus; if (!mHaveBatteryLevel) { mHaveBatteryLevel = true; @@ -4837,7 +6008,19 @@ public final class BatteryStatsImpl extends BatteryStats { } if (onBattery) { mDischargeCurrentLevel = level; - mRecordingHistory = true; + if (!mRecordingHistory) { + mRecordingHistory = true; + startRecordingHistory(elapsedRealtime, uptime, true); + } + } else if (level < 96) { + if (!mRecordingHistory) { + mRecordingHistory = true; + startRecordingHistory(elapsedRealtime, uptime, true); + } + } + mCurrentBatteryLevel = level; + if (mDischargePlugLevel < 0) { + mDischargePlugLevel = level; } if (onBattery != mOnBattery) { mHistoryCur.batteryLevel = (byte)level; @@ -4846,7 +6029,7 @@ public final class BatteryStatsImpl extends BatteryStats { mHistoryCur.batteryPlugType = (byte)plugType; mHistoryCur.batteryTemperature = (short)temp; mHistoryCur.batteryVoltage = (char)volt; - setOnBatteryLocked(onBattery, oldStatus, level); + setOnBatteryLocked(elapsedRealtime, uptime, onBattery, oldStatus, level); } else { boolean changed = false; if (mHistoryCur.batteryLevel != level) { @@ -4876,13 +6059,30 @@ public final class BatteryStatsImpl extends BatteryStats { changed = true; } if (changed) { - addHistoryRecordLocked(SystemClock.elapsedRealtime()); + addHistoryRecordLocked(elapsedRealtime, uptime); + } + if (onBattery) { + if (mLastDischargeStepLevel != level) { + mNumDischargeStepDurations = addLevelSteps(mDischargeStepDurations, + mNumDischargeStepDurations, mLastDischargeStepTime, + mLastDischargeStepLevel - level, elapsedRealtime); + mLastDischargeStepLevel = level; + mLastDischargeStepTime = elapsedRealtime; + } + } else { + if (mLastChargeStepLevel != level) { + mNumChargeStepDurations = addLevelSteps(mChargeStepDurations, + mNumChargeStepDurations, mLastChargeStepTime, + level - mLastChargeStepLevel, elapsedRealtime); + mLastChargeStepLevel = level; + mLastChargeStepTime = elapsedRealtime; + } } } if (!onBattery && status == BatteryManager.BATTERY_STATUS_FULL) { // We don't record history while we are plugged in and fully charged. // The next time we are unplugged, history will be cleared. - mRecordingHistory = false; + mRecordingHistory = DEBUG; } } } @@ -4902,8 +6102,8 @@ public final class BatteryStatsImpl extends BatteryStats { SamplingTimer kwlt = mKernelWakelockStats.get(name); if (kwlt == null) { - kwlt = new SamplingTimer(mUnpluggables, mOnBatteryInternal, - true /* track reported values */); + kwlt = new SamplingTimer(mOnBatteryScreenOffTimeBase, + true /* track reported val */); mKernelWakelockStats.put(name, kwlt); } kwlt.updateCurrentReportedCount(kws.mCount); @@ -4922,48 +6122,124 @@ public final class BatteryStatsImpl extends BatteryStats { } } - private void updateNetworkActivityLocked() { + static final int NET_UPDATE_MOBILE = 1<<0; + static final int NET_UPDATE_WIFI = 1<<1; + static final int NET_UPDATE_ALL = 0xffff; + + private void updateNetworkActivityLocked(int which, long elapsedRealtimeMs) { if (!SystemProperties.getBoolean(PROP_QTAGUID_ENABLED, false)) return; - final NetworkStats snapshot; - try { - snapshot = mNetworkStatsFactory.readNetworkStatsDetail(); - } catch (IOException e) { - Log.wtf(TAG, "Failed to read network stats", e); - return; - } + if ((which&NET_UPDATE_MOBILE) != 0 && mMobileIfaces.length > 0) { + final NetworkStats snapshot; + final NetworkStats last = mCurMobileSnapshot; + try { + snapshot = mNetworkStatsFactory.readNetworkStatsDetail(UID_ALL, + mMobileIfaces, NetworkStats.TAG_NONE, mLastMobileSnapshot); + } catch (IOException e) { + Log.wtf(TAG, "Failed to read mobile network stats", e); + return; + } - if (mLastSnapshot == null) { - mLastSnapshot = snapshot; - return; - } + mCurMobileSnapshot = snapshot; + mLastMobileSnapshot = last; - final NetworkStats delta = snapshot.subtract(mLastSnapshot); - mLastSnapshot = snapshot; + if (mOnBatteryInternal) { + final NetworkStats delta = NetworkStats.subtract(snapshot, last, + null, null, mTmpNetworkStats); + mTmpNetworkStats = delta; + + long radioTime = mMobileRadioActivePerAppTimer.checkpointRunningLocked( + elapsedRealtimeMs); + long totalPackets = delta.getTotalPackets(); + + final int size = delta.size(); + for (int i = 0; i < size; i++) { + final NetworkStats.Entry entry = delta.getValues(i, mTmpNetworkStatsEntry); + + if (entry.rxBytes == 0 || entry.txBytes == 0) continue; + + final Uid u = getUidStatsLocked(mapUid(entry.uid)); + u.noteNetworkActivityLocked(NETWORK_MOBILE_RX_DATA, entry.rxBytes, + entry.rxPackets); + u.noteNetworkActivityLocked(NETWORK_MOBILE_TX_DATA, entry.txBytes, + entry.txPackets); + + if (radioTime > 0) { + // Distribute total radio active time in to this app. + long appPackets = entry.rxPackets + entry.txPackets; + long appRadioTime = (radioTime*appPackets)/totalPackets; + u.noteMobileRadioActiveTimeLocked(appRadioTime); + // Remove this app from the totals, so that we don't lose any time + // due to rounding. + radioTime -= appRadioTime; + totalPackets -= appPackets; + } - NetworkStats.Entry entry = null; - final int size = delta.size(); - for (int i = 0; i < size; i++) { - entry = delta.getValues(i, entry); + mNetworkByteActivityCounters[NETWORK_MOBILE_RX_DATA].addCountLocked( + entry.rxBytes); + mNetworkByteActivityCounters[NETWORK_MOBILE_TX_DATA].addCountLocked( + entry.txBytes); + mNetworkPacketActivityCounters[NETWORK_MOBILE_RX_DATA].addCountLocked( + entry.rxPackets); + mNetworkPacketActivityCounters[NETWORK_MOBILE_TX_DATA].addCountLocked( + entry.txPackets); + } - if (entry.rxBytes == 0 || entry.txBytes == 0) continue; - if (entry.tag != NetworkStats.TAG_NONE) continue; + if (radioTime > 0) { + // Whoops, there is some radio time we can't blame on an app! + mMobileRadioActiveUnknownTime.addCountLocked(radioTime); + mMobileRadioActiveUnknownCount.addCountLocked(1); + } + } + } - final Uid u = getUidStatsLocked(entry.uid); + if ((which&NET_UPDATE_WIFI) != 0 && mWifiIfaces.length > 0) { + final NetworkStats snapshot; + final NetworkStats last = mCurWifiSnapshot; + try { + snapshot = mNetworkStatsFactory.readNetworkStatsDetail(UID_ALL, + mWifiIfaces, NetworkStats.TAG_NONE, mLastWifiSnapshot); + } catch (IOException e) { + Log.wtf(TAG, "Failed to read wifi network stats", e); + return; + } - if (mMobileIfaces.contains(entry.iface)) { - u.noteNetworkActivityLocked(NETWORK_MOBILE_RX_BYTES, entry.rxBytes); - u.noteNetworkActivityLocked(NETWORK_MOBILE_TX_BYTES, entry.txBytes); + mCurWifiSnapshot = snapshot; + mLastWifiSnapshot = last; - mNetworkActivityCounters[NETWORK_MOBILE_RX_BYTES].addCountLocked(entry.rxBytes); - mNetworkActivityCounters[NETWORK_MOBILE_TX_BYTES].addCountLocked(entry.txBytes); + if (mOnBatteryInternal) { + final NetworkStats delta = NetworkStats.subtract(snapshot, last, + null, null, mTmpNetworkStats); + mTmpNetworkStats = delta; + + final int size = delta.size(); + for (int i = 0; i < size; i++) { + final NetworkStats.Entry entry = delta.getValues(i, mTmpNetworkStatsEntry); + + if (DEBUG) { + final NetworkStats.Entry cur = snapshot.getValues(i, null); + Slog.d(TAG, "Wifi uid " + entry.uid + ": delta rx=" + entry.rxBytes + + " tx=" + entry.txBytes + ", cur rx=" + cur.rxBytes + + " tx=" + cur.txBytes); + } + + if (entry.rxBytes == 0 || entry.txBytes == 0) continue; - } else if (mWifiIfaces.contains(entry.iface)) { - u.noteNetworkActivityLocked(NETWORK_WIFI_RX_BYTES, entry.rxBytes); - u.noteNetworkActivityLocked(NETWORK_WIFI_TX_BYTES, entry.txBytes); + final Uid u = getUidStatsLocked(mapUid(entry.uid)); + u.noteNetworkActivityLocked(NETWORK_WIFI_RX_DATA, entry.rxBytes, + entry.rxPackets); + u.noteNetworkActivityLocked(NETWORK_WIFI_TX_DATA, entry.txBytes, + entry.txPackets); - mNetworkActivityCounters[NETWORK_WIFI_RX_BYTES].addCountLocked(entry.rxBytes); - mNetworkActivityCounters[NETWORK_WIFI_TX_BYTES].addCountLocked(entry.txBytes); + mNetworkByteActivityCounters[NETWORK_WIFI_RX_DATA].addCountLocked( + entry.rxBytes); + mNetworkByteActivityCounters[NETWORK_WIFI_TX_DATA].addCountLocked( + entry.txBytes); + mNetworkPacketActivityCounters[NETWORK_WIFI_RX_DATA].addCountLocked( + entry.rxPackets); + mNetworkPacketActivityCounters[NETWORK_WIFI_TX_DATA].addCountLocked( + entry.txPackets); + } } } } @@ -4980,9 +6256,8 @@ public final class BatteryStatsImpl extends BatteryStats { public long computeUptime(long curTime, int which) { switch (which) { case STATS_SINCE_CHARGED: return mUptime + (curTime-mUptimeStart); - case STATS_LAST: return mLastUptime; case STATS_CURRENT: return (curTime-mUptimeStart); - case STATS_SINCE_UNPLUGGED: return (curTime-mTrackBatteryUptimeStart); + case STATS_SINCE_UNPLUGGED: return (curTime-mOnBatteryTimeBase.getUptimeStart()); } return 0; } @@ -4991,71 +6266,155 @@ public final class BatteryStatsImpl extends BatteryStats { public long computeRealtime(long curTime, int which) { switch (which) { case STATS_SINCE_CHARGED: return mRealtime + (curTime-mRealtimeStart); - case STATS_LAST: return mLastRealtime; case STATS_CURRENT: return (curTime-mRealtimeStart); - case STATS_SINCE_UNPLUGGED: return (curTime-mTrackBatteryRealtimeStart); + case STATS_SINCE_UNPLUGGED: return (curTime-mOnBatteryTimeBase.getRealtimeStart()); } return 0; } @Override public long computeBatteryUptime(long curTime, int which) { - switch (which) { - case STATS_SINCE_CHARGED: - return mBatteryUptime + getBatteryUptime(curTime); - case STATS_LAST: - return mBatteryLastUptime; - case STATS_CURRENT: - return getBatteryUptime(curTime); - case STATS_SINCE_UNPLUGGED: - return getBatteryUptimeLocked(curTime) - mUnpluggedBatteryUptime; - } - return 0; + return mOnBatteryTimeBase.computeUptime(curTime, which); } @Override public long computeBatteryRealtime(long curTime, int which) { - switch (which) { - case STATS_SINCE_CHARGED: - return mBatteryRealtime + getBatteryRealtimeLocked(curTime); - case STATS_LAST: - return mBatteryLastRealtime; - case STATS_CURRENT: - return getBatteryRealtimeLocked(curTime); - case STATS_SINCE_UNPLUGGED: - return getBatteryRealtimeLocked(curTime) - mUnpluggedBatteryRealtime; + return mOnBatteryTimeBase.computeRealtime(curTime, which); + } + + @Override + public long computeBatteryScreenOffUptime(long curTime, int which) { + return mOnBatteryScreenOffTimeBase.computeUptime(curTime, which); + } + + @Override + public long computeBatteryScreenOffRealtime(long curTime, int which) { + return mOnBatteryScreenOffTimeBase.computeRealtime(curTime, which); + } + + private long computeTimePerLevel(long[] steps, int numSteps) { + // For now we'll do a simple average across all steps. + if (numSteps <= 0) { + return -1; } - return 0; + long total = 0; + for (int i=0; i<numSteps; i++) { + total += steps[i]; + } + return total / numSteps; + /* + long[] buckets = new long[numSteps]; + int numBuckets = 0; + int numToAverage = 4; + int i = 0; + while (i < numSteps) { + long totalTime = 0; + int num = 0; + for (int j=0; j<numToAverage && (i+j)<numSteps; j++) { + totalTime += steps[i+j]; + num++; + } + buckets[numBuckets] = totalTime / num; + numBuckets++; + numToAverage *= 2; + i += num; + } + if (numBuckets < 1) { + return -1; + } + long averageTime = buckets[numBuckets-1]; + for (i=numBuckets-2; i>=0; i--) { + averageTime = (averageTime + buckets[i]) / 2; + } + return averageTime; + */ } - long getBatteryUptimeLocked(long curTime) { - long time = mTrackBatteryPastUptime; - if (mOnBatteryInternal) { - time += curTime - mTrackBatteryUptimeStart; + @Override + public long computeBatteryTimeRemaining(long curTime) { + if (!mOnBattery) { + return -1; + } + /* Simple implementation just looks at the average discharge per level across the + entire sample period. + int discharge = (getLowDischargeAmountSinceCharge()+getHighDischargeAmountSinceCharge())/2; + if (discharge < 2) { + return -1; } - return time; + long duration = computeBatteryRealtime(curTime, STATS_SINCE_CHARGED); + if (duration < 1000*1000) { + return -1; + } + long usPerLevel = duration/discharge; + return usPerLevel * mCurrentBatteryLevel; + */ + if (mNumDischargeStepDurations < 1) { + return -1; + } + long msPerLevel = computeTimePerLevel(mDischargeStepDurations, mNumDischargeStepDurations); + if (msPerLevel <= 0) { + return -1; + } + return (msPerLevel * mCurrentBatteryLevel) * 1000; } - long getBatteryUptimeLocked() { - return getBatteryUptime(SystemClock.uptimeMillis() * 1000); + public int getNumDischargeStepDurations() { + return mNumDischargeStepDurations; } - @Override - public long getBatteryUptime(long curTime) { - return getBatteryUptimeLocked(curTime); + public long[] getDischargeStepDurationsArray() { + return mDischargeStepDurations; } - long getBatteryRealtimeLocked(long curTime) { - long time = mTrackBatteryPastRealtime; - if (mOnBatteryInternal) { - time += curTime - mTrackBatteryRealtimeStart; + @Override + public long computeChargeTimeRemaining(long curTime) { + if (mOnBattery) { + // Not yet working. + return -1; + } + /* Broken + int curLevel = mCurrentBatteryLevel; + int plugLevel = mDischargePlugLevel; + if (plugLevel < 0 || curLevel < (plugLevel+1)) { + return -1; } - return time; + long duration = computeBatteryRealtime(curTime, STATS_SINCE_UNPLUGGED); + if (duration < 1000*1000) { + return -1; + } + long usPerLevel = duration/(curLevel-plugLevel); + return usPerLevel * (100-curLevel); + */ + if (mNumChargeStepDurations < 1) { + return -1; + } + long msPerLevel = computeTimePerLevel(mChargeStepDurations, mNumChargeStepDurations); + if (msPerLevel <= 0) { + return -1; + } + return (msPerLevel * (100-mCurrentBatteryLevel)) * 1000; + } + + public int getNumChargeStepDurations() { + return mNumChargeStepDurations; + } + + public long[] getChargeStepDurationsArray() { + return mChargeStepDurations; + } + + long getBatteryUptimeLocked() { + return mOnBatteryTimeBase.getUptime(SystemClock.uptimeMillis() * 1000); + } + + @Override + public long getBatteryUptime(long curTime) { + return mOnBatteryTimeBase.getUptime(curTime); } @Override public long getBatteryRealtime(long curTime) { - return getBatteryRealtimeLocked(curTime); + return mOnBatteryTimeBase.getRealtime(curTime); } @Override @@ -5101,7 +6460,18 @@ public final class BatteryStatsImpl extends BatteryStats { return val; } } - + + @Override + public int getDischargeAmount(int which) { + int dischargeAmount = which == STATS_SINCE_CHARGED + ? getHighDischargeAmountSinceCharge() + : (getDischargeStartLevel() - getDischargeCurrentLevel()); + if (dischargeAmount < 0) { + dischargeAmount = 0; + } + return dischargeAmount; + } + public int getDischargeAmountScreenOn() { synchronized(this) { int val = mDischargeAmountScreenOn; @@ -5175,24 +6545,7 @@ public final class BatteryStatsImpl extends BatteryStats { * if needed. */ public Uid.Proc getProcessStatsLocked(int uid, String name) { - Uid u = getUidStatsLocked(uid); - return u.getProcessStatsLocked(name); - } - - /** - * Retrieve the statistics object for a particular process, given - * the name of the process. - * @param name process name - * @return the statistics object for the process - */ - public Uid.Proc getProcessStatsLocked(String name, int pid) { - int uid; - if (mUidCache.containsKey(name)) { - uid = mUidCache.get(name); - } else { - uid = Process.getUidForPid(pid); - mUidCache.put(name, uid); - } + uid = mapUid(uid); Uid u = getUidStatsLocked(uid); return u.getProcessStatsLocked(name); } @@ -5202,6 +6555,7 @@ public final class BatteryStatsImpl extends BatteryStats { * if needed. */ public Uid.Pkg getPackageStatsLocked(int uid, String pkg) { + uid = mapUid(uid); Uid u = getUidStatsLocked(uid); return u.getPackageStatsLocked(pkg); } @@ -5211,6 +6565,7 @@ public final class BatteryStatsImpl extends BatteryStats { * if needed. */ public Uid.Pkg.Serv getServiceStatsLocked(int uid, String pkg, String name) { + uid = mapUid(uid); Uid u = getUidStatsLocked(uid); return u.getServiceStatsLocked(pkg, name); } @@ -5251,7 +6606,7 @@ public final class BatteryStatsImpl extends BatteryStats { time = (time*uidRunningTime)/totalRunningTime; SamplingCounter uidSc = uidProc.mSpeedBins[sb]; if (uidSc == null) { - uidSc = new SamplingCounter(mUnpluggables); + uidSc = new SamplingCounter(mOnBatteryTimeBase); uidProc.mSpeedBins[sb] = uidSc; } uidSc.mCount.addAndGet((int)time); @@ -5388,15 +6743,20 @@ public final class BatteryStatsImpl extends BatteryStats { stream.close(); readSummaryFromParcel(in); - } catch(java.io.IOException e) { + } catch(Exception e) { Slog.e("BatteryStats", "Error reading battery statistics", e); } - long now = SystemClock.elapsedRealtime(); - if (USE_OLD_HISTORY) { - addHistoryRecordLocked(now, HistoryItem.CMD_START); + if (mHistoryBuffer.dataPosition() > 0) { + mRecordingHistory = true; + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); + if (USE_OLD_HISTORY) { + addHistoryRecordLocked(elapsedRealtime, uptime, HistoryItem.CMD_START, mHistoryCur); + } + addHistoryBufferLocked(elapsedRealtime, uptime, HistoryItem.CMD_START, mHistoryCur); + startRecordingHistory(elapsedRealtime, uptime, false); } - addHistoryBufferLocked(now, HistoryItem.CMD_START); } public int describeContents() { @@ -5408,6 +6768,25 @@ public final class BatteryStatsImpl extends BatteryStats { mHistoryBuffer.setDataSize(0); mHistoryBuffer.setDataPosition(0); + mHistoryTagPool.clear(); + mNextHistoryTagIdx = 0; + mNumHistoryTagChars = 0; + + int numTags = in.readInt(); + for (int i=0; i<numTags; i++) { + int idx = in.readInt(); + String str = in.readString(); + int uid = in.readInt(); + HistoryTag tag = new HistoryTag(); + tag.string = str; + tag.uid = uid; + tag.poolIdx = idx; + mHistoryTagPool.put(tag, idx); + if (idx >= mNextHistoryTagIdx) { + mNextHistoryTagIdx = idx+1; + } + mNumHistoryTagChars += tag.string.length() + 1; + } int bufSize = in.readInt(); int curPos = in.dataPosition(); @@ -5471,11 +6850,18 @@ public final class BatteryStatsImpl extends BatteryStats { StringBuilder sb = new StringBuilder(128); sb.append("****************** WRITING mHistoryBaseTime: "); TimeUtils.formatDuration(mHistoryBaseTime, sb); - sb.append(" mLastHistoryTime: "); - TimeUtils.formatDuration(mLastHistoryTime, sb); + sb.append(" mLastHistoryElapsedRealtime: "); + TimeUtils.formatDuration(mLastHistoryElapsedRealtime, sb); Slog.i(TAG, sb.toString()); } - out.writeLong(mHistoryBaseTime + mLastHistoryTime); + out.writeLong(mHistoryBaseTime + mLastHistoryElapsedRealtime); + out.writeInt(mHistoryTagPool.size()); + for (HashMap.Entry<HistoryTag, Integer> ent : mHistoryTagPool.entrySet()) { + HistoryTag tag = ent.getKey(); + out.writeInt(ent.getValue()); + out.writeString(tag.string); + out.writeInt(tag.uid); + } out.writeInt(mHistoryBuffer.dataSize()); if (DEBUG_HISTORY) Slog.i(TAG, "***************** WRITING HISTORY: " + mHistoryBuffer.dataSize() + " bytes at " + out.dataPosition()); @@ -5509,16 +6895,23 @@ public final class BatteryStatsImpl extends BatteryStats { readHistory(in, true); mStartCount = in.readInt(); - mBatteryUptime = in.readLong(); - mBatteryRealtime = in.readLong(); mUptime = in.readLong(); mRealtime = in.readLong(); + mStartClockTime = in.readLong(); + mOnBatteryTimeBase.readSummaryFromParcel(in); + mOnBatteryScreenOffTimeBase.readSummaryFromParcel(in); mDischargeUnplugLevel = in.readInt(); + mDischargePlugLevel = in.readInt(); mDischargeCurrentLevel = in.readInt(); + mCurrentBatteryLevel = in.readInt(); mLowDischargeAmountSinceCharge = in.readInt(); mHighDischargeAmountSinceCharge = in.readInt(); mDischargeAmountScreenOnSinceCharge = in.readInt(); mDischargeAmountScreenOffSinceCharge = in.readInt(); + mNumDischargeStepDurations = in.readInt(); + in.readLongArray(mDischargeStepDurations); + mNumChargeStepDurations = in.readInt(); + in.readLongArray(mChargeStepDurations); mStartCount++; @@ -5538,14 +6931,27 @@ public final class BatteryStatsImpl extends BatteryStats { mPhoneDataConnectionsTimer[i].readSummaryFromParcelLocked(in); } for (int i = 0; i < NUM_NETWORK_ACTIVITY_TYPES; i++) { - mNetworkActivityCounters[i].readSummaryFromParcelLocked(in); - } + mNetworkByteActivityCounters[i].readSummaryFromParcelLocked(in); + mNetworkPacketActivityCounters[i].readSummaryFromParcelLocked(in); + } + mMobileRadioPowerState = DataConnectionRealTimeInfo.DC_POWER_STATE_LOW; + mMobileRadioActiveTimer.readSummaryFromParcelLocked(in); + mMobileRadioActivePerAppTimer.readSummaryFromParcelLocked(in); + mMobileRadioActiveAdjustedTime.readSummaryFromParcelLocked(in); + mMobileRadioActiveUnknownTime.readSummaryFromParcelLocked(in); + mMobileRadioActiveUnknownCount.readSummaryFromParcelLocked(in); mWifiOn = false; mWifiOnTimer.readSummaryFromParcelLocked(in); mGlobalWifiRunning = false; mGlobalWifiRunningTimer.readSummaryFromParcelLocked(in); + for (int i=0; i<NUM_WIFI_STATES; i++) { + mWifiStateTimer[i].readSummaryFromParcelLocked(in); + } mBluetoothOn = false; mBluetoothOnTimer.readSummaryFromParcelLocked(in); + for (int i=0; i< NUM_BLUETOOTH_STATES; i++) { + mBluetoothStateTimer[i].readSummaryFromParcelLocked(in); + } int NKW = in.readInt(); if (NKW > 10000) { @@ -5559,7 +6965,22 @@ public final class BatteryStatsImpl extends BatteryStats { } } + int NWR = in.readInt(); + if (NWR > 10000) { + Slog.w(TAG, "File corrupt: too many wakeup reasons " + NWR); + return; + } + for (int iwr = 0; iwr < NWR; iwr++) { + if (in.readInt() != 0) { + String reasonName = in.readString(); + getWakeupReasonCounterLocked(reasonName).readSummaryFromParcelLocked(in); + } + } + sNumSpeedSteps = in.readInt(); + if (sNumSpeedSteps < 0 || sNumSpeedSteps > 100) { + throw new BadParcelableException("Bad speed steps in data: " + sNumSpeedSteps); + } final int NU = in.readInt(); if (NU > 10000) { @@ -5619,12 +7040,15 @@ public final class BatteryStatsImpl extends BatteryStats { } if (in.readInt() != 0) { - if (u.mNetworkActivityCounters == null) { + if (u.mNetworkByteActivityCounters == null) { u.initNetworkActivityLocked(); } for (int i = 0; i < NUM_NETWORK_ACTIVITY_TYPES; i++) { - u.mNetworkActivityCounters[i].readSummaryFromParcelLocked(in); + u.mNetworkByteActivityCounters[i].readSummaryFromParcelLocked(in); + u.mNetworkPacketActivityCounters[i].readSummaryFromParcelLocked(in); } + u.mMobileRadioActiveTime.readSummaryFromParcelLocked(in); + u.mMobileRadioActiveCount.readSummaryFromParcelLocked(in); } int NW = in.readInt(); @@ -5678,7 +7102,7 @@ public final class BatteryStatsImpl extends BatteryStats { p.mSpeedBins = new SamplingCounter[NSB]; for (int i=0; i<NSB; i++) { if (in.readInt() != 0) { - p.mSpeedBins[i] = new SamplingCounter(mUnpluggables); + p.mSpeedBins[i] = new SamplingCounter(mOnBatteryTimeBase); p.mSpeedBins[i].readSummaryFromParcelLocked(in); } } @@ -5719,50 +7143,65 @@ public final class BatteryStatsImpl extends BatteryStats { * @param out the Parcel to be written to. */ public void writeSummaryToParcel(Parcel out) { - // Need to update with current kernel wake lock counts. - updateKernelWakelocksLocked(); - updateNetworkActivityLocked(); + pullPendingStateUpdatesLocked(); final long NOW_SYS = SystemClock.uptimeMillis() * 1000; final long NOWREAL_SYS = SystemClock.elapsedRealtime() * 1000; - final long NOW = getBatteryUptimeLocked(NOW_SYS); - final long NOWREAL = getBatteryRealtimeLocked(NOWREAL_SYS); out.writeInt(VERSION); writeHistory(out, true); out.writeInt(mStartCount); - out.writeLong(computeBatteryUptime(NOW_SYS, STATS_SINCE_CHARGED)); - out.writeLong(computeBatteryRealtime(NOWREAL_SYS, STATS_SINCE_CHARGED)); out.writeLong(computeUptime(NOW_SYS, STATS_SINCE_CHARGED)); out.writeLong(computeRealtime(NOWREAL_SYS, STATS_SINCE_CHARGED)); + out.writeLong(mStartClockTime); + mOnBatteryTimeBase.writeSummaryToParcel(out, NOW_SYS, NOWREAL_SYS); + mOnBatteryScreenOffTimeBase.writeSummaryToParcel(out, NOW_SYS, NOWREAL_SYS); out.writeInt(mDischargeUnplugLevel); + out.writeInt(mDischargePlugLevel); out.writeInt(mDischargeCurrentLevel); + out.writeInt(mCurrentBatteryLevel); out.writeInt(getLowDischargeAmountSinceCharge()); out.writeInt(getHighDischargeAmountSinceCharge()); out.writeInt(getDischargeAmountScreenOnSinceCharge()); out.writeInt(getDischargeAmountScreenOffSinceCharge()); - - mScreenOnTimer.writeSummaryFromParcelLocked(out, NOWREAL); + out.writeInt(mNumDischargeStepDurations); + out.writeLongArray(mDischargeStepDurations); + out.writeInt(mNumChargeStepDurations); + out.writeLongArray(mChargeStepDurations); + + mScreenOnTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS); for (int i=0; i<NUM_SCREEN_BRIGHTNESS_BINS; i++) { - mScreenBrightnessTimer[i].writeSummaryFromParcelLocked(out, NOWREAL); + mScreenBrightnessTimer[i].writeSummaryFromParcelLocked(out, NOWREAL_SYS); } mInputEventCounter.writeSummaryFromParcelLocked(out); - mPhoneOnTimer.writeSummaryFromParcelLocked(out, NOWREAL); + mPhoneOnTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS); for (int i=0; i<SignalStrength.NUM_SIGNAL_STRENGTH_BINS; i++) { - mPhoneSignalStrengthsTimer[i].writeSummaryFromParcelLocked(out, NOWREAL); + mPhoneSignalStrengthsTimer[i].writeSummaryFromParcelLocked(out, NOWREAL_SYS); } - mPhoneSignalScanningTimer.writeSummaryFromParcelLocked(out, NOWREAL); + mPhoneSignalScanningTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS); for (int i=0; i<NUM_DATA_CONNECTION_TYPES; i++) { - mPhoneDataConnectionsTimer[i].writeSummaryFromParcelLocked(out, NOWREAL); + mPhoneDataConnectionsTimer[i].writeSummaryFromParcelLocked(out, NOWREAL_SYS); } for (int i = 0; i < NUM_NETWORK_ACTIVITY_TYPES; i++) { - mNetworkActivityCounters[i].writeSummaryFromParcelLocked(out); + mNetworkByteActivityCounters[i].writeSummaryFromParcelLocked(out); + mNetworkPacketActivityCounters[i].writeSummaryFromParcelLocked(out); + } + mMobileRadioActiveTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS); + mMobileRadioActivePerAppTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS); + mMobileRadioActiveAdjustedTime.writeSummaryFromParcelLocked(out); + mMobileRadioActiveUnknownTime.writeSummaryFromParcelLocked(out); + mMobileRadioActiveUnknownCount.writeSummaryFromParcelLocked(out); + mWifiOnTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS); + mGlobalWifiRunningTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS); + for (int i=0; i<NUM_WIFI_STATES; i++) { + mWifiStateTimer[i].writeSummaryFromParcelLocked(out, NOWREAL_SYS); + } + mBluetoothOnTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS); + for (int i=0; i< NUM_BLUETOOTH_STATES; i++) { + mBluetoothStateTimer[i].writeSummaryFromParcelLocked(out, NOWREAL_SYS); } - mWifiOnTimer.writeSummaryFromParcelLocked(out, NOWREAL); - mGlobalWifiRunningTimer.writeSummaryFromParcelLocked(out, NOWREAL); - mBluetoothOnTimer.writeSummaryFromParcelLocked(out, NOWREAL); out.writeInt(mKernelWakelockStats.size()); for (Map.Entry<String, SamplingTimer> ent : mKernelWakelockStats.entrySet()) { @@ -5770,7 +7209,19 @@ public final class BatteryStatsImpl extends BatteryStats { if (kwlt != null) { out.writeInt(1); out.writeString(ent.getKey()); - ent.getValue().writeSummaryFromParcelLocked(out, NOWREAL); + kwlt.writeSummaryFromParcelLocked(out, NOWREAL_SYS); + } else { + out.writeInt(0); + } + } + + out.writeInt(mWakeupReasonStats.size()); + for (Map.Entry<String, LongSamplingCounter> ent : mWakeupReasonStats.entrySet()) { + LongSamplingCounter counter = ent.getValue(); + if (counter != null) { + out.writeInt(1); + out.writeString(ent.getKey()); + counter.writeSummaryFromParcelLocked(out); } else { out.writeInt(0); } @@ -5785,57 +7236,57 @@ public final class BatteryStatsImpl extends BatteryStats { if (u.mWifiRunningTimer != null) { out.writeInt(1); - u.mWifiRunningTimer.writeSummaryFromParcelLocked(out, NOWREAL); + u.mWifiRunningTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS); } else { out.writeInt(0); } if (u.mFullWifiLockTimer != null) { out.writeInt(1); - u.mFullWifiLockTimer.writeSummaryFromParcelLocked(out, NOWREAL); + u.mFullWifiLockTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS); } else { out.writeInt(0); } if (u.mWifiScanTimer != null) { out.writeInt(1); - u.mWifiScanTimer.writeSummaryFromParcelLocked(out, NOWREAL); + u.mWifiScanTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS); } else { out.writeInt(0); } for (int i = 0; i < Uid.NUM_WIFI_BATCHED_SCAN_BINS; i++) { if (u.mWifiBatchedScanTimer[i] != null) { out.writeInt(1); - u.mWifiBatchedScanTimer[i].writeSummaryFromParcelLocked(out, NOWREAL); + u.mWifiBatchedScanTimer[i].writeSummaryFromParcelLocked(out, NOWREAL_SYS); } else { out.writeInt(0); } } if (u.mWifiMulticastTimer != null) { out.writeInt(1); - u.mWifiMulticastTimer.writeSummaryFromParcelLocked(out, NOWREAL); + u.mWifiMulticastTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS); } else { out.writeInt(0); } if (u.mAudioTurnedOnTimer != null) { out.writeInt(1); - u.mAudioTurnedOnTimer.writeSummaryFromParcelLocked(out, NOWREAL); + u.mAudioTurnedOnTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS); } else { out.writeInt(0); } if (u.mVideoTurnedOnTimer != null) { out.writeInt(1); - u.mVideoTurnedOnTimer.writeSummaryFromParcelLocked(out, NOWREAL); + u.mVideoTurnedOnTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS); } else { out.writeInt(0); } if (u.mForegroundActivityTimer != null) { out.writeInt(1); - u.mForegroundActivityTimer.writeSummaryFromParcelLocked(out, NOWREAL); + u.mForegroundActivityTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS); } else { out.writeInt(0); } if (u.mVibratorOnTimer != null) { out.writeInt(1); - u.mVibratorOnTimer.writeSummaryFromParcelLocked(out, NOWREAL); + u.mVibratorOnTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS); } else { out.writeInt(0); } @@ -5849,13 +7300,16 @@ public final class BatteryStatsImpl extends BatteryStats { } } - if (u.mNetworkActivityCounters == null) { + if (u.mNetworkByteActivityCounters == null) { out.writeInt(0); } else { out.writeInt(1); for (int i = 0; i < NUM_NETWORK_ACTIVITY_TYPES; i++) { - u.mNetworkActivityCounters[i].writeSummaryFromParcelLocked(out); + u.mNetworkByteActivityCounters[i].writeSummaryFromParcelLocked(out); + u.mNetworkPacketActivityCounters[i].writeSummaryFromParcelLocked(out); } + u.mMobileRadioActiveTime.writeSummaryFromParcelLocked(out); + u.mMobileRadioActiveCount.writeSummaryFromParcelLocked(out); } int NW = u.mWakelockStats.size(); @@ -5867,19 +7321,19 @@ public final class BatteryStatsImpl extends BatteryStats { Uid.Wakelock wl = ent.getValue(); if (wl.mTimerFull != null) { out.writeInt(1); - wl.mTimerFull.writeSummaryFromParcelLocked(out, NOWREAL); + wl.mTimerFull.writeSummaryFromParcelLocked(out, NOWREAL_SYS); } else { out.writeInt(0); } if (wl.mTimerPartial != null) { out.writeInt(1); - wl.mTimerPartial.writeSummaryFromParcelLocked(out, NOWREAL); + wl.mTimerPartial.writeSummaryFromParcelLocked(out, NOWREAL_SYS); } else { out.writeInt(0); } if (wl.mTimerWindow != null) { out.writeInt(1); - wl.mTimerWindow.writeSummaryFromParcelLocked(out, NOWREAL); + wl.mTimerWindow.writeSummaryFromParcelLocked(out, NOWREAL_SYS); } else { out.writeInt(0); } @@ -5895,7 +7349,7 @@ public final class BatteryStatsImpl extends BatteryStats { Uid.Sensor se = ent.getValue(); if (se.mTimer != null) { out.writeInt(1); - se.mTimer.writeSummaryFromParcelLocked(out, NOWREAL); + se.mTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS); } else { out.writeInt(0); } @@ -5942,7 +7396,8 @@ public final class BatteryStatsImpl extends BatteryStats { : ps.mServiceStats.entrySet()) { out.writeString(sent.getKey()); BatteryStatsImpl.Uid.Pkg.Serv ss = sent.getValue(); - long time = ss.getStartTimeToNowLocked(NOW); + long time = ss.getStartTimeToNowLocked( + mOnBatteryTimeBase.getUptime(NOW_SYS)); out.writeLong(time); out.writeInt(ss.mStarts); out.writeInt(ss.mLaunches); @@ -5966,64 +7421,75 @@ public final class BatteryStatsImpl extends BatteryStats { readHistory(in, false); mStartCount = in.readInt(); - mBatteryUptime = in.readLong(); - mBatteryLastUptime = 0; - mBatteryRealtime = in.readLong(); - mBatteryLastRealtime = 0; + mStartClockTime = in.readLong(); + mUptime = in.readLong(); + mUptimeStart = in.readLong(); + mRealtime = in.readLong(); + mRealtimeStart = in.readLong(); + mOnBattery = in.readInt() != 0; + mOnBatteryInternal = false; // we are no longer really running. + mOnBatteryTimeBase.readFromParcel(in); + mOnBatteryScreenOffTimeBase.readFromParcel(in); + mScreenOn = false; - mScreenOnTimer = new StopwatchTimer(null, -1, null, mUnpluggables, in); + mScreenOnTimer = new StopwatchTimer(null, -1, null, mOnBatteryTimeBase, in); for (int i=0; i<NUM_SCREEN_BRIGHTNESS_BINS; i++) { - mScreenBrightnessTimer[i] = new StopwatchTimer(null, -100-i, - null, mUnpluggables, in); + mScreenBrightnessTimer[i] = new StopwatchTimer(null, -100-i, null, mOnBatteryTimeBase, + in); } - mInputEventCounter = new Counter(mUnpluggables, in); + mInputEventCounter = new Counter(mOnBatteryTimeBase, in); mPhoneOn = false; - mPhoneOnTimer = new StopwatchTimer(null, -2, null, mUnpluggables, in); + mPhoneOnTimer = new StopwatchTimer(null, -2, null, mOnBatteryTimeBase, in); for (int i=0; i<SignalStrength.NUM_SIGNAL_STRENGTH_BINS; i++) { mPhoneSignalStrengthsTimer[i] = new StopwatchTimer(null, -200-i, - null, mUnpluggables, in); + null, mOnBatteryTimeBase, in); } - mPhoneSignalScanningTimer = new StopwatchTimer(null, -200+1, null, mUnpluggables, in); + mPhoneSignalScanningTimer = new StopwatchTimer(null, -200+1, null, mOnBatteryTimeBase, in); for (int i=0; i<NUM_DATA_CONNECTION_TYPES; i++) { mPhoneDataConnectionsTimer[i] = new StopwatchTimer(null, -300-i, - null, mUnpluggables, in); + null, mOnBatteryTimeBase, in); } for (int i = 0; i < NUM_NETWORK_ACTIVITY_TYPES; i++) { - mNetworkActivityCounters[i] = new LongSamplingCounter(mUnpluggables, in); - } + mNetworkByteActivityCounters[i] = new LongSamplingCounter(mOnBatteryTimeBase, in); + mNetworkPacketActivityCounters[i] = new LongSamplingCounter(mOnBatteryTimeBase, in); + } + mMobileRadioPowerState = DataConnectionRealTimeInfo.DC_POWER_STATE_LOW; + mMobileRadioActiveTimer = new StopwatchTimer(null, -400, null, mOnBatteryTimeBase, in); + mMobileRadioActivePerAppTimer = new StopwatchTimer(null, -401, null, mOnBatteryTimeBase, + in); + mMobileRadioActiveAdjustedTime = new LongSamplingCounter(mOnBatteryTimeBase, in); + mMobileRadioActiveUnknownTime = new LongSamplingCounter(mOnBatteryTimeBase, in); + mMobileRadioActiveUnknownCount = new LongSamplingCounter(mOnBatteryTimeBase, in); mWifiOn = false; - mWifiOnTimer = new StopwatchTimer(null, -2, null, mUnpluggables, in); + mWifiOnTimer = new StopwatchTimer(null, -2, null, mOnBatteryTimeBase, in); mGlobalWifiRunning = false; - mGlobalWifiRunningTimer = new StopwatchTimer(null, -2, null, mUnpluggables, in); + mGlobalWifiRunningTimer = new StopwatchTimer(null, -2, null, mOnBatteryTimeBase, in); + for (int i=0; i<NUM_WIFI_STATES; i++) { + mWifiStateTimer[i] = new StopwatchTimer(null, -600-i, + null, mOnBatteryTimeBase, in); + } mBluetoothOn = false; - mBluetoothOnTimer = new StopwatchTimer(null, -2, null, mUnpluggables, in); - mUptime = in.readLong(); - mUptimeStart = in.readLong(); - mLastUptime = 0; - mRealtime = in.readLong(); - mRealtimeStart = in.readLong(); - mLastRealtime = 0; - mOnBattery = in.readInt() != 0; - mOnBatteryInternal = false; // we are no longer really running. - mTrackBatteryPastUptime = in.readLong(); - mTrackBatteryUptimeStart = in.readLong(); - mTrackBatteryPastRealtime = in.readLong(); - mTrackBatteryRealtimeStart = in.readLong(); - mUnpluggedBatteryUptime = in.readLong(); - mUnpluggedBatteryRealtime = in.readLong(); + mBluetoothOnTimer = new StopwatchTimer(null, -2, null, mOnBatteryTimeBase, in); + for (int i=0; i< NUM_BLUETOOTH_STATES; i++) { + mBluetoothStateTimer[i] = new StopwatchTimer(null, -500-i, + null, mOnBatteryTimeBase, in); + } mDischargeUnplugLevel = in.readInt(); + mDischargePlugLevel = in.readInt(); mDischargeCurrentLevel = in.readInt(); + mCurrentBatteryLevel = in.readInt(); mLowDischargeAmountSinceCharge = in.readInt(); mHighDischargeAmountSinceCharge = in.readInt(); mDischargeAmountScreenOn = in.readInt(); mDischargeAmountScreenOnSinceCharge = in.readInt(); mDischargeAmountScreenOff = in.readInt(); mDischargeAmountScreenOffSinceCharge = in.readInt(); + mNumDischargeStepDurations = in.readInt(); + in.readLongArray(mDischargeStepDurations); + mNumChargeStepDurations = in.readInt(); + in.readLongArray(mChargeStepDurations); mLastWriteTime = in.readLong(); - mRadioDataUptime = in.readLong(); - mRadioDataStart = -1; - mBluetoothPingCount = in.readInt(); mBluetoothPingStart = -1; @@ -6032,12 +7498,22 @@ public final class BatteryStatsImpl extends BatteryStats { for (int ikw = 0; ikw < NKW; ikw++) { if (in.readInt() != 0) { String wakelockName = in.readString(); - in.readInt(); // Extra 0/1 written by Timer.writeTimerToParcel - SamplingTimer kwlt = new SamplingTimer(mUnpluggables, mOnBattery, in); + SamplingTimer kwlt = new SamplingTimer(mOnBatteryTimeBase, in); mKernelWakelockStats.put(wakelockName, kwlt); } } + mWakeupReasonStats.clear(); + int NWR = in.readInt(); + for (int iwr = 0; iwr < NWR; iwr++) { + if (in.readInt() != 0) { + String reasonName = in.readString(); + LongSamplingCounter counter = new LongSamplingCounter(mOnBatteryScreenOffTimeBase, + in); + mWakeupReasonStats.put(reasonName, counter); + } + } + mPartialTimers.clear(); mFullTimers.clear(); mWindowTimers.clear(); @@ -6054,7 +7530,7 @@ public final class BatteryStatsImpl extends BatteryStats { for (int i = 0; i < numUids; i++) { int uid = in.readInt(); Uid u = new Uid(uid); - u.readFromParcelLocked(mUnpluggables, in); + u.readFromParcelLocked(mOnBatteryTimeBase, mOnBatteryScreenOffTimeBase, in); mUidStats.append(uid, u); } } @@ -6070,64 +7546,74 @@ public final class BatteryStatsImpl extends BatteryStats { @SuppressWarnings("unused") void writeToParcelLocked(Parcel out, boolean inclUids, int flags) { // Need to update with current kernel wake lock counts. - updateKernelWakelocksLocked(); - updateNetworkActivityLocked(); + pullPendingStateUpdatesLocked(); final long uSecUptime = SystemClock.uptimeMillis() * 1000; final long uSecRealtime = SystemClock.elapsedRealtime() * 1000; - final long batteryUptime = getBatteryUptimeLocked(uSecUptime); - final long batteryRealtime = getBatteryRealtimeLocked(uSecRealtime); + final long batteryRealtime = mOnBatteryTimeBase.getRealtime(uSecRealtime); + final long batteryScreenOffRealtime = mOnBatteryScreenOffTimeBase.getRealtime(uSecRealtime); out.writeInt(MAGIC); writeHistory(out, false); out.writeInt(mStartCount); - out.writeLong(mBatteryUptime); - out.writeLong(mBatteryRealtime); - mScreenOnTimer.writeToParcel(out, batteryRealtime); + out.writeLong(mStartClockTime); + out.writeLong(mUptime); + out.writeLong(mUptimeStart); + out.writeLong(mRealtime); + out.writeLong(mRealtimeStart); + out.writeInt(mOnBattery ? 1 : 0); + mOnBatteryTimeBase.writeToParcel(out, uSecUptime, uSecRealtime); + mOnBatteryScreenOffTimeBase.writeToParcel(out, uSecUptime, uSecRealtime); + + mScreenOnTimer.writeToParcel(out, uSecRealtime); for (int i=0; i<NUM_SCREEN_BRIGHTNESS_BINS; i++) { - mScreenBrightnessTimer[i].writeToParcel(out, batteryRealtime); + mScreenBrightnessTimer[i].writeToParcel(out, uSecRealtime); } mInputEventCounter.writeToParcel(out); - mPhoneOnTimer.writeToParcel(out, batteryRealtime); + mPhoneOnTimer.writeToParcel(out, uSecRealtime); for (int i=0; i<SignalStrength.NUM_SIGNAL_STRENGTH_BINS; i++) { - mPhoneSignalStrengthsTimer[i].writeToParcel(out, batteryRealtime); + mPhoneSignalStrengthsTimer[i].writeToParcel(out, uSecRealtime); } - mPhoneSignalScanningTimer.writeToParcel(out, batteryRealtime); + mPhoneSignalScanningTimer.writeToParcel(out, uSecRealtime); for (int i=0; i<NUM_DATA_CONNECTION_TYPES; i++) { - mPhoneDataConnectionsTimer[i].writeToParcel(out, batteryRealtime); + mPhoneDataConnectionsTimer[i].writeToParcel(out, uSecRealtime); } for (int i = 0; i < NUM_NETWORK_ACTIVITY_TYPES; i++) { - mNetworkActivityCounters[i].writeToParcel(out); + mNetworkByteActivityCounters[i].writeToParcel(out); + mNetworkPacketActivityCounters[i].writeToParcel(out); + } + mMobileRadioActiveTimer.writeToParcel(out, uSecRealtime); + mMobileRadioActivePerAppTimer.writeToParcel(out, uSecRealtime); + mMobileRadioActiveAdjustedTime.writeToParcel(out); + mMobileRadioActiveUnknownTime.writeToParcel(out); + mMobileRadioActiveUnknownCount.writeToParcel(out); + mWifiOnTimer.writeToParcel(out, uSecRealtime); + mGlobalWifiRunningTimer.writeToParcel(out, uSecRealtime); + for (int i=0; i<NUM_WIFI_STATES; i++) { + mWifiStateTimer[i].writeToParcel(out, uSecRealtime); + } + mBluetoothOnTimer.writeToParcel(out, uSecRealtime); + for (int i=0; i< NUM_BLUETOOTH_STATES; i++) { + mBluetoothStateTimer[i].writeToParcel(out, uSecRealtime); } - mWifiOnTimer.writeToParcel(out, batteryRealtime); - mGlobalWifiRunningTimer.writeToParcel(out, batteryRealtime); - mBluetoothOnTimer.writeToParcel(out, batteryRealtime); - out.writeLong(mUptime); - out.writeLong(mUptimeStart); - out.writeLong(mRealtime); - out.writeLong(mRealtimeStart); - out.writeInt(mOnBattery ? 1 : 0); - out.writeLong(batteryUptime); - out.writeLong(mTrackBatteryUptimeStart); - out.writeLong(batteryRealtime); - out.writeLong(mTrackBatteryRealtimeStart); - out.writeLong(mUnpluggedBatteryUptime); - out.writeLong(mUnpluggedBatteryRealtime); out.writeInt(mDischargeUnplugLevel); + out.writeInt(mDischargePlugLevel); out.writeInt(mDischargeCurrentLevel); + out.writeInt(mCurrentBatteryLevel); out.writeInt(mLowDischargeAmountSinceCharge); out.writeInt(mHighDischargeAmountSinceCharge); out.writeInt(mDischargeAmountScreenOn); out.writeInt(mDischargeAmountScreenOnSinceCharge); out.writeInt(mDischargeAmountScreenOff); out.writeInt(mDischargeAmountScreenOffSinceCharge); + out.writeInt(mNumDischargeStepDurations); + out.writeLongArray(mDischargeStepDurations); + out.writeInt(mNumChargeStepDurations); + out.writeLongArray(mChargeStepDurations); out.writeLong(mLastWriteTime); - // Write radio uptime for data - out.writeLong(getRadioDataUptime()); - out.writeInt(getBluetoothPingCount()); if (inclUids) { @@ -6137,7 +7623,18 @@ public final class BatteryStatsImpl extends BatteryStats { if (kwlt != null) { out.writeInt(1); out.writeString(ent.getKey()); - Timer.writeTimerToParcel(out, kwlt, batteryRealtime); + kwlt.writeToParcel(out, uSecRealtime); + } else { + out.writeInt(0); + } + } + out.writeInt(mWakeupReasonStats.size()); + for (Map.Entry<String, LongSamplingCounter> ent : mWakeupReasonStats.entrySet()) { + LongSamplingCounter counter = ent.getValue(); + if (counter != null) { + out.writeInt(1); + out.writeString(ent.getKey()); + counter.writeToParcel(out); } else { out.writeInt(0); } @@ -6155,7 +7652,7 @@ public final class BatteryStatsImpl extends BatteryStats { out.writeInt(mUidStats.keyAt(i)); Uid uid = mUidStats.valueAt(i); - uid.writeToParcelLocked(out, batteryRealtime); + uid.writeToParcelLocked(out, uSecRealtime); } } else { out.writeInt(0); @@ -6175,12 +7672,15 @@ public final class BatteryStatsImpl extends BatteryStats { public void prepareForDumpLocked() { // Need to retrieve current kernel wake lock stats before printing. - updateKernelWakelocksLocked(); - updateNetworkActivityLocked(); + pullPendingStateUpdatesLocked(); } - public void dumpLocked(PrintWriter pw, boolean isUnpluggedOnly, int reqUid) { + public void dumpLocked(Context context, PrintWriter pw, int flags, int reqUid, long histStart) { if (DEBUG) { + pw.println("mOnBatteryTimeBase:"); + mOnBatteryTimeBase.dump(pw, " "); + pw.println("mOnBatteryScreenOffTimeBase:"); + mOnBatteryScreenOffTimeBase.dump(pw, " "); Printer pr = new PrintWriterPrinter(pw); pr.println("*** Screen timer:"); mScreenOnTimer.logState(pr, " "); @@ -6202,13 +7702,26 @@ public final class BatteryStatsImpl extends BatteryStats { pr.println("*** Data connection type #" + i + ":"); mPhoneDataConnectionsTimer[i].logState(pr, " "); } + pr.println("*** mMobileRadioPowerState=" + mMobileRadioPowerState); + pr.println("*** Mobile network active timer:"); + mMobileRadioActiveTimer.logState(pr, " "); + pr.println("*** Mobile network active adjusted timer:"); + mMobileRadioActiveAdjustedTime.logState(pr, " "); pr.println("*** Wifi timer:"); mWifiOnTimer.logState(pr, " "); pr.println("*** WifiRunning timer:"); mGlobalWifiRunningTimer.logState(pr, " "); + for (int i=0; i<NUM_WIFI_STATES; i++) { + pr.println("*** Wifi state #" + i + ":"); + mWifiStateTimer[i].logState(pr, " "); + } pr.println("*** Bluetooth timer:"); mBluetoothOnTimer.logState(pr, " "); + for (int i=0; i< NUM_BLUETOOTH_STATES; i++) { + pr.println("*** Bluetooth active type #" + i + ":"); + mBluetoothStateTimer[i].logState(pr, " "); + } } - super.dumpLocked(pw, isUnpluggedOnly, reqUid); + super.dumpLocked(context, pw, flags, reqUid, histStart); } } diff --git a/core/java/com/android/internal/os/HandlerCaller.java b/core/java/com/android/internal/os/HandlerCaller.java index d9e3ef6..40834ba 100644 --- a/core/java/com/android/internal/os/HandlerCaller.java +++ b/core/java/com/android/internal/os/HandlerCaller.java @@ -85,7 +85,27 @@ public class HandlerCaller { public void sendMessage(Message msg) { mH.sendMessage(msg); } - + + public SomeArgs sendMessageAndWait(Message msg) { + if (Looper.myLooper() == mH.getLooper()) { + throw new IllegalStateException("Can't wait on same thread as looper"); + } + SomeArgs args = (SomeArgs)msg.obj; + args.mWaitState = SomeArgs.WAIT_WAITING; + mH.sendMessage(msg); + synchronized (args) { + while (args.mWaitState == SomeArgs.WAIT_WAITING) { + try { + args.wait(); + } catch (InterruptedException e) { + return null; + } + } + } + args.mWaitState = SomeArgs.WAIT_NONE; + return args; + } + public Message obtainMessage(int what) { return mH.obtainMessage(what); } @@ -136,6 +156,14 @@ public class HandlerCaller { return mH.obtainMessage(what, arg1, 0, args); } + public Message obtainMessageIOOO(int what, int arg1, Object arg2, Object arg3, Object arg4) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = arg2; + args.arg2 = arg3; + args.arg3 = arg4; + return mH.obtainMessage(what, arg1, 0, args); + } + public Message obtainMessageOO(int what, Object arg1, Object arg2) { SomeArgs args = SomeArgs.obtain(); args.arg1 = arg1; @@ -161,6 +189,17 @@ public class HandlerCaller { return mH.obtainMessage(what, 0, 0, args); } + public Message obtainMessageOOOOO(int what, Object arg1, Object arg2, + Object arg3, Object arg4, Object arg5) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = arg1; + args.arg2 = arg2; + args.arg3 = arg3; + args.arg4 = arg4; + args.arg5 = arg5; + return mH.obtainMessage(what, 0, 0, args); + } + public Message obtainMessageIIII(int what, int arg1, int arg2, int arg3, int arg4) { SomeArgs args = SomeArgs.obtain(); diff --git a/core/java/com/android/internal/os/SomeArgs.java b/core/java/com/android/internal/os/SomeArgs.java index 6fb72f1..7edf4cc 100644 --- a/core/java/com/android/internal/os/SomeArgs.java +++ b/core/java/com/android/internal/os/SomeArgs.java @@ -35,6 +35,11 @@ public final class SomeArgs { private boolean mInPool; + static final int WAIT_NONE = 0; + static final int WAIT_WAITING = 1; + static final int WAIT_FINISHED = 2; + int mWaitState = WAIT_NONE; + public Object arg1; public Object arg2; public Object arg3; @@ -70,6 +75,9 @@ public final class SomeArgs { if (mInPool) { throw new IllegalStateException("Already recycled."); } + if (mWaitState != WAIT_NONE) { + return; + } synchronized (sPoolLock) { clear(); if (sPoolSize < MAX_POOL_SIZE) { diff --git a/core/java/com/android/internal/policy/IKeyguardService.aidl b/core/java/com/android/internal/policy/IKeyguardService.aidl index 63ff5a0..b78c70f 100644 --- a/core/java/com/android/internal/policy/IKeyguardService.aidl +++ b/core/java/com/android/internal/policy/IKeyguardService.aidl @@ -25,12 +25,24 @@ import android.os.Bundle; interface IKeyguardService { boolean isShowing(); boolean isSecure(); - boolean isShowingAndNotHidden(); + boolean isShowingAndNotOccluded(); boolean isInputRestricted(); boolean isDismissable(); oneway void verifyUnlock(IKeyguardExitCallback callback); oneway void keyguardDone(boolean authenticated, boolean wakeup); - oneway void setHidden(boolean isHidden); + + /** + * Sets the Keyguard as occluded when a window dismisses the Keyguard with flag + * FLAG_SHOW_ON_LOCK_SCREEN. + * + * @param isOccluded Whether the Keyguard is occluded by another window. + * @return See IKeyguardServiceConstants.KEYGUARD_SERVICE_SET_OCCLUDED_*. This is needed because + * PhoneWindowManager needs to set these flags immediately and can't wait for the + * Keyguard thread to pick it up. In the hidden case, PhoneWindowManager is solely + * responsible to make sure that the flags are unset. + */ + int setOccluded(boolean isOccluded); + oneway void dismiss(); oneway void onDreamingStarted(); oneway void onDreamingStopped(); diff --git a/core/java/com/android/internal/policy/IKeyguardServiceConstants.java b/core/java/com/android/internal/policy/IKeyguardServiceConstants.java new file mode 100644 index 0000000..b88174a --- /dev/null +++ b/core/java/com/android/internal/policy/IKeyguardServiceConstants.java @@ -0,0 +1,41 @@ +/* + * 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.internal.policy; + +/** + * @hide + */ +public class IKeyguardServiceConstants { + + /** + * Constant for {@link com.android.internal.policy.IKeyguardService#setHidden(boolean)}: + * Don't change the keyguard window flags. + */ + public static final int KEYGUARD_SERVICE_SET_OCCLUDED_RESULT_NONE = 0; + + /** + * Constant for {@link com.android.internal.policy.IKeyguardService#setHidden(boolean)}: + * Set the keyguard window flags to FLAG_SHOW_WALLPAPER and PRIVATE_FLAG_KEYGUARD. + */ + public static final int KEYGUARD_SERVICE_SET_OCCLUDED_RESULT_SET_FLAGS = 1; + + /** + * Constant for {@link com.android.internal.policy.IKeyguardService#setHidden(boolean)}: + * Unset the keyguard window flags to FLAG_SHOW_WALLPAPER and PRIVATE_FLAG_KEYGUARD. + */ + public static final int KEYGUARD_SERVICE_SET_OCCLUDED_RESULT_UNSET_FLAGS = 2; +} diff --git a/core/java/com/android/internal/preference/YesNoPreference.java b/core/java/com/android/internal/preference/YesNoPreference.java index cf68a58..7abf416 100644 --- a/core/java/com/android/internal/preference/YesNoPreference.java +++ b/core/java/com/android/internal/preference/YesNoPreference.java @@ -31,15 +31,19 @@ import android.util.AttributeSet; */ public class YesNoPreference extends DialogPreference { private boolean mWasPositiveResult; - - public YesNoPreference(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + + public YesNoPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public YesNoPreference(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); } public YesNoPreference(Context context, AttributeSet attrs) { this(context, attrs, com.android.internal.R.attr.yesNoPreferenceStyle); } - + public YesNoPreference(Context context) { this(context, null); } diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl index 97ea7d8..6428e15 100644 --- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl @@ -41,11 +41,14 @@ interface IStatusBarService out List<IBinder> notificationKeys, out List<StatusBarNotification> notifications, out int[] switches, out List<IBinder> binders); void onPanelRevealed(); - void onNotificationClick(String pkg, String tag, int id); + void onPanelHidden(); + void onNotificationClick(String pkg, String tag, int id, int userId); void onNotificationError(String pkg, String tag, int id, - int uid, int initialPid, String message); - void onClearAllNotifications(); - void onNotificationClear(String pkg, String tag, int id); + int uid, int initialPid, String message, int userId); + void onClearAllNotifications(int userId); + void onNotificationClear(String pkg, String tag, int id, int userId); + void onNotificationVisibilityChanged( + in String[] newlyVisibleKeys, in String[] noLongerVisibleKeys); void setSystemUiVisibility(int vis, int mask); void setHardKeyboardEnabled(boolean enabled); void toggleRecentApps(); diff --git a/core/java/com/android/internal/util/ArrayUtils.java b/core/java/com/android/internal/util/ArrayUtils.java index 9137d3c..d177410 100644 --- a/core/java/com/android/internal/util/ArrayUtils.java +++ b/core/java/com/android/internal/util/ArrayUtils.java @@ -16,11 +16,10 @@ package com.android.internal.util; -import java.lang.reflect.Array; +import dalvik.system.VMRuntime; +import libcore.util.EmptyArray; -// XXX these should be changed to reflect the actual memory allocator we use. -// it looks like right now objects want to be powers of 2 minus 8 -// and the array size eats another 4 bytes +import java.lang.reflect.Array; /** * ArrayUtils contains some methods that you can call to find out @@ -28,46 +27,42 @@ import java.lang.reflect.Array; */ public class ArrayUtils { - private static Object[] EMPTY = new Object[0]; private static final int CACHE_SIZE = 73; private static Object[] sCache = new Object[CACHE_SIZE]; private ArrayUtils() { /* cannot be instantiated */ } - public static int idealByteArraySize(int need) { - for (int i = 4; i < 32; i++) - if (need <= (1 << i) - 12) - return (1 << i) - 12; - - return need; + public static byte[] newUnpaddedByteArray(int minLen) { + return (byte[])VMRuntime.getRuntime().newUnpaddedArray(byte.class, minLen); } - public static int idealBooleanArraySize(int need) { - return idealByteArraySize(need); + public static char[] newUnpaddedCharArray(int minLen) { + return (char[])VMRuntime.getRuntime().newUnpaddedArray(char.class, minLen); } - public static int idealShortArraySize(int need) { - return idealByteArraySize(need * 2) / 2; + public static int[] newUnpaddedIntArray(int minLen) { + return (int[])VMRuntime.getRuntime().newUnpaddedArray(int.class, minLen); } - public static int idealCharArraySize(int need) { - return idealByteArraySize(need * 2) / 2; + public static boolean[] newUnpaddedBooleanArray(int minLen) { + return (boolean[])VMRuntime.getRuntime().newUnpaddedArray(boolean.class, minLen); } - public static int idealIntArraySize(int need) { - return idealByteArraySize(need * 4) / 4; + public static long[] newUnpaddedLongArray(int minLen) { + return (long[])VMRuntime.getRuntime().newUnpaddedArray(long.class, minLen); } - public static int idealFloatArraySize(int need) { - return idealByteArraySize(need * 4) / 4; + public static float[] newUnpaddedFloatArray(int minLen) { + return (float[])VMRuntime.getRuntime().newUnpaddedArray(float.class, minLen); } - public static int idealObjectArraySize(int need) { - return idealByteArraySize(need * 4) / 4; + public static Object[] newUnpaddedObjectArray(int minLen) { + return (Object[])VMRuntime.getRuntime().newUnpaddedArray(Object.class, minLen); } - public static int idealLongArraySize(int need) { - return idealByteArraySize(need * 8) / 8; + @SuppressWarnings("unchecked") + public static <T> T[] newUnpaddedArray(Class<T> clazz, int minLen) { + return (T[])VMRuntime.getRuntime().newUnpaddedArray(clazz, minLen); } /** @@ -102,9 +97,10 @@ public class ArrayUtils * it will return the same empty array every time to avoid reallocation, * although this is not guaranteed. */ + @SuppressWarnings("unchecked") public static <T> T[] emptyArray(Class<T> kind) { if (kind == Object.class) { - return (T[]) EMPTY; + return (T[]) EmptyArray.OBJECT; } int bucket = ((System.identityHashCode(kind) / 8) & 0x7FFFFFFF) % CACHE_SIZE; diff --git a/core/java/com/android/internal/util/GrowingArrayUtils.java b/core/java/com/android/internal/util/GrowingArrayUtils.java new file mode 100644 index 0000000..b4d2d730 --- /dev/null +++ b/core/java/com/android/internal/util/GrowingArrayUtils.java @@ -0,0 +1,196 @@ +/* + * 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.internal.util; + +/** + * A helper class that aims to provide comparable growth performance to ArrayList, but on primitive + * arrays. Common array operations are implemented for efficient use in dynamic containers. + * + * All methods in this class assume that the length of an array is equivalent to its capacity and + * NOT the number of elements in the array. The current size of the array is always passed in as a + * parameter. + * + * @hide + */ +public final class GrowingArrayUtils { + + /** + * Appends an element to the end of the array, growing the array if there is no more room. + * @param array The array to which to append the element. This must NOT be null. + * @param currentSize The number of elements in the array. Must be less than or equal to + * array.length. + * @param element The element to append. + * @return the array to which the element was appended. This may be different than the given + * array. + */ + public static <T> T[] append(T[] array, int currentSize, T element) { + assert currentSize <= array.length; + + if (currentSize + 1 > array.length) { + @SuppressWarnings("unchecked") + T[] newArray = ArrayUtils.newUnpaddedArray( + (Class<T>) array.getClass().getComponentType(), growSize(currentSize)); + System.arraycopy(array, 0, newArray, 0, currentSize); + array = newArray; + } + array[currentSize] = element; + return array; + } + + /** + * Primitive int version of {@link #append(Object[], int, Object)}. + */ + public static int[] append(int[] array, int currentSize, int element) { + assert currentSize <= array.length; + + if (currentSize + 1 > array.length) { + int[] newArray = ArrayUtils.newUnpaddedIntArray(growSize(currentSize)); + System.arraycopy(array, 0, newArray, 0, currentSize); + array = newArray; + } + array[currentSize] = element; + return array; + } + + /** + * Primitive long version of {@link #append(Object[], int, Object)}. + */ + public static long[] append(long[] array, int currentSize, long element) { + assert currentSize <= array.length; + + if (currentSize + 1 > array.length) { + long[] newArray = ArrayUtils.newUnpaddedLongArray(growSize(currentSize)); + System.arraycopy(array, 0, newArray, 0, currentSize); + array = newArray; + } + array[currentSize] = element; + return array; + } + + /** + * Primitive boolean version of {@link #append(Object[], int, Object)}. + */ + public static boolean[] append(boolean[] array, int currentSize, boolean element) { + assert currentSize <= array.length; + + if (currentSize + 1 > array.length) { + boolean[] newArray = ArrayUtils.newUnpaddedBooleanArray(growSize(currentSize)); + System.arraycopy(array, 0, newArray, 0, currentSize); + array = newArray; + } + array[currentSize] = element; + return array; + } + + /** + * Inserts an element into the array at the specified index, growing the array if there is no + * more room. + * + * @param array The array to which to append the element. Must NOT be null. + * @param currentSize The number of elements in the array. Must be less than or equal to + * array.length. + * @param element The element to insert. + * @return the array to which the element was appended. This may be different than the given + * array. + */ + public static <T> T[] insert(T[] array, int currentSize, int index, T element) { + assert currentSize <= array.length; + + if (currentSize + 1 <= array.length) { + System.arraycopy(array, index, array, index + 1, currentSize - index); + array[index] = element; + return array; + } + + @SuppressWarnings("unchecked") + T[] newArray = ArrayUtils.newUnpaddedArray((Class<T>)array.getClass().getComponentType(), + growSize(currentSize)); + System.arraycopy(array, 0, newArray, 0, index); + newArray[index] = element; + System.arraycopy(array, index, newArray, index + 1, array.length - index); + return newArray; + } + + /** + * Primitive int version of {@link #insert(Object[], int, int, Object)}. + */ + public static int[] insert(int[] array, int currentSize, int index, int element) { + assert currentSize <= array.length; + + if (currentSize + 1 <= array.length) { + System.arraycopy(array, index, array, index + 1, currentSize - index); + array[index] = element; + return array; + } + + int[] newArray = ArrayUtils.newUnpaddedIntArray(growSize(currentSize)); + System.arraycopy(array, 0, newArray, 0, index); + newArray[index] = element; + System.arraycopy(array, index, newArray, index + 1, array.length - index); + return newArray; + } + + /** + * Primitive long version of {@link #insert(Object[], int, int, Object)}. + */ + public static long[] insert(long[] array, int currentSize, int index, long element) { + assert currentSize <= array.length; + + if (currentSize + 1 <= array.length) { + System.arraycopy(array, index, array, index + 1, currentSize - index); + array[index] = element; + return array; + } + + long[] newArray = ArrayUtils.newUnpaddedLongArray(growSize(currentSize)); + System.arraycopy(array, 0, newArray, 0, index); + newArray[index] = element; + System.arraycopy(array, index, newArray, index + 1, array.length - index); + return newArray; + } + + /** + * Primitive boolean version of {@link #insert(Object[], int, int, Object)}. + */ + public static boolean[] insert(boolean[] array, int currentSize, int index, boolean element) { + assert currentSize <= array.length; + + if (currentSize + 1 <= array.length) { + System.arraycopy(array, index, array, index + 1, currentSize - index); + array[index] = element; + return array; + } + + boolean[] newArray = ArrayUtils.newUnpaddedBooleanArray(growSize(currentSize)); + System.arraycopy(array, 0, newArray, 0, index); + newArray[index] = element; + System.arraycopy(array, index, newArray, index + 1, array.length - index); + return newArray; + } + + /** + * Given the current size of an array, returns an ideal size to which the array should grow. + * This is typically double the given size, but should not be relied upon to do so in the + * future. + */ + public static int growSize(int currentSize) { + return currentSize <= 4 ? 8 : currentSize * 2; + } + + // Uninstantiable + private GrowingArrayUtils() {} +} diff --git a/core/java/com/android/internal/util/ImageUtils.java b/core/java/com/android/internal/util/ImageUtils.java new file mode 100644 index 0000000..a5ce6e0 --- /dev/null +++ b/core/java/com/android/internal/util/ImageUtils.java @@ -0,0 +1,84 @@ +/* + * 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.internal.util; + +import android.graphics.Bitmap; + +/** + * Utility class for image analysis and processing. + * + * @hide + */ +public class ImageUtils { + + // Amount (max is 255) that two channels can differ before the color is no longer "gray". + private static final int TOLERANCE = 20; + + // Alpha amount for which values below are considered transparent. + private static final int ALPHA_TOLERANCE = 50; + + private int[] mTempBuffer; + + /** + * Checks whether a bitmap is grayscale. Grayscale here means "very close to a perfect + * gray". + */ + public boolean isGrayscale(Bitmap bitmap) { + final int height = bitmap.getHeight(); + final int width = bitmap.getWidth(); + int size = height*width; + + ensureBufferSize(size); + bitmap.getPixels(mTempBuffer, 0, width, 0, 0, width, height); + for (int i = 0; i < size; i++) { + if (!isGrayscale(mTempBuffer[i])) { + return false; + } + } + return true; + } + + /** + * Makes sure that {@code mTempBuffer} has at least length {@code size}. + */ + private void ensureBufferSize(int size) { + if (mTempBuffer == null || mTempBuffer.length < size) { + mTempBuffer = new int[size]; + } + } + + /** + * Classifies a color as grayscale or not. Grayscale here means "very close to a perfect + * gray"; if all three channels are approximately equal, this will return true. + * + * Note that really transparent colors are always grayscale. + */ + public static boolean isGrayscale(int color) { + int alpha = 0xFF & (color >> 24); + if (alpha < ALPHA_TOLERANCE) { + return true; + } + + int r = 0xFF & (color >> 16); + int g = 0xFF & (color >> 8); + int b = 0xFF & color; + + return Math.abs(r - g) < TOLERANCE + && Math.abs(r - b) < TOLERANCE + && Math.abs(g - b) < TOLERANCE; + } +} diff --git a/core/java/com/android/internal/util/LegacyNotificationUtil.java b/core/java/com/android/internal/util/LegacyNotificationUtil.java new file mode 100644 index 0000000..0394bbc --- /dev/null +++ b/core/java/com/android/internal/util/LegacyNotificationUtil.java @@ -0,0 +1,193 @@ +/* + * 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.internal.util; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.drawable.AnimationDrawable; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.TextAppearanceSpan; +import android.util.Log; +import android.util.Pair; + +import java.util.Arrays; +import java.util.WeakHashMap; + +/** + * Helper class to process legacy (Holo) notifications to make them look like quantum notifications. + * + * @hide + */ +public class LegacyNotificationUtil { + + private static final String TAG = "LegacyNotificationUtil"; + + private static final Object sLock = new Object(); + private static LegacyNotificationUtil sInstance; + + private final ImageUtils mImageUtils = new ImageUtils(); + private final WeakHashMap<Bitmap, Pair<Boolean, Integer>> mGrayscaleBitmapCache = + new WeakHashMap<Bitmap, Pair<Boolean, Integer>>(); + + public static LegacyNotificationUtil getInstance() { + synchronized (sLock) { + if (sInstance == null) { + sInstance = new LegacyNotificationUtil(); + } + return sInstance; + } + } + + /** + * Checks whether a bitmap is grayscale. Grayscale here means "very close to a perfect + * gray". + * + * @param bitmap The bitmap to test. + * @return Whether the bitmap is grayscale. + */ + public boolean isGrayscale(Bitmap bitmap) { + synchronized (sLock) { + Pair<Boolean, Integer> cached = mGrayscaleBitmapCache.get(bitmap); + if (cached != null) { + if (cached.second == bitmap.getGenerationId()) { + return cached.first; + } + } + } + boolean result; + int generationId; + synchronized (mImageUtils) { + result = mImageUtils.isGrayscale(bitmap); + + // generationId and the check whether the Bitmap is grayscale can't be read atomically + // here. However, since the thread is in the process of posting the notification, we can + // assume that it doesn't modify the bitmap while we are checking the pixels. + generationId = bitmap.getGenerationId(); + } + synchronized (sLock) { + mGrayscaleBitmapCache.put(bitmap, Pair.create(result, generationId)); + } + return result; + } + + /** + * Checks whether a drawable is grayscale. Grayscale here means "very close to a perfect + * gray". + * + * @param d The drawable to test. + * @return Whether the drawable is grayscale. + */ + public boolean isGrayscale(Drawable d) { + if (d == null) { + return false; + } else if (d instanceof BitmapDrawable) { + BitmapDrawable bd = (BitmapDrawable) d; + return bd.getBitmap() != null && isGrayscale(bd.getBitmap()); + } else if (d instanceof AnimationDrawable) { + AnimationDrawable ad = (AnimationDrawable) d; + int count = ad.getNumberOfFrames(); + return count > 0 && isGrayscale(ad.getFrame(0)); + } else { + return false; + } + } + + /** + * Checks whether a drawable with a resoure id is grayscale. Grayscale here means "very close + * to a perfect gray". + * + * @param context The context to load the drawable from. + * @return Whether the drawable is grayscale. + */ + public boolean isGrayscale(Context context, int drawableResId) { + if (drawableResId != 0) { + try { + return isGrayscale(context.getDrawable(drawableResId)); + } catch (Resources.NotFoundException ex) { + Log.e(TAG, "Drawable not found: " + drawableResId); + return false; + } + } else { + return false; + } + } + + /** + * Inverts all the grayscale colors set by {@link android.text.style.TextAppearanceSpan}s on + * the text. + * + * @param charSequence The text to process. + * @return The color inverted text. + */ + public CharSequence invertCharSequenceColors(CharSequence charSequence) { + if (charSequence instanceof Spanned) { + Spanned ss = (Spanned) charSequence; + Object[] spans = ss.getSpans(0, ss.length(), Object.class); + SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString()); + for (Object span : spans) { + Object resultSpan = span; + if (span instanceof TextAppearanceSpan) { + resultSpan = processTextAppearanceSpan((TextAppearanceSpan) span); + } + builder.setSpan(resultSpan, ss.getSpanStart(span), ss.getSpanEnd(span), + ss.getSpanFlags(span)); + } + return builder; + } + return charSequence; + } + + private TextAppearanceSpan processTextAppearanceSpan(TextAppearanceSpan span) { + ColorStateList colorStateList = span.getTextColor(); + if (colorStateList != null) { + int[] colors = colorStateList.getColors(); + boolean changed = false; + for (int i = 0; i < colors.length; i++) { + if (ImageUtils.isGrayscale(colors[i])) { + + // Allocate a new array so we don't change the colors in the old color state + // list. + if (!changed) { + colors = Arrays.copyOf(colors, colors.length); + } + colors[i] = processColor(colors[i]); + changed = true; + } + } + if (changed) { + return new TextAppearanceSpan( + span.getFamily(), span.getTextStyle(), span.getTextSize(), + new ColorStateList(colorStateList.getStates(), colors), + span.getLinkTextColor()); + } + } + return span; + } + + private int processColor(int color) { + return Color.argb(Color.alpha(color), + 255 - Color.red(color), + 255 - Color.green(color), + 255 - Color.blue(color)); + } +} diff --git a/core/java/com/android/internal/view/ActionBarPolicy.java b/core/java/com/android/internal/view/ActionBarPolicy.java index 25086c5..bee59dc 100644 --- a/core/java/com/android/internal/view/ActionBarPolicy.java +++ b/core/java/com/android/internal/view/ActionBarPolicy.java @@ -19,11 +19,9 @@ package com.android.internal.view; import com.android.internal.R; import android.content.Context; -import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; import android.os.Build; -import android.view.ViewConfiguration; /** * Allows components to query for various configuration policy decisions diff --git a/core/java/com/android/internal/view/IInputMethodClient.aidl b/core/java/com/android/internal/view/IInputMethodClient.aidl index ce4312d..9e8d12b 100644 --- a/core/java/com/android/internal/view/IInputMethodClient.aidl +++ b/core/java/com/android/internal/view/IInputMethodClient.aidl @@ -27,4 +27,5 @@ oneway interface IInputMethodClient { void onBindMethod(in InputBindResult res); void onUnbindMethod(int sequence); void setActive(boolean active); + void setCursorAnchorMonitorMode(int monitorMode); } diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl index 3724a08..5336174 100644 --- a/core/java/com/android/internal/view/IInputMethodManager.aidl +++ b/core/java/com/android/internal/view/IInputMethodManager.aidl @@ -32,7 +32,9 @@ import com.android.internal.view.IInputMethodClient; * this file. */ interface IInputMethodManager { + // TODO: Use ParceledListSlice instead List<InputMethodInfo> getInputMethodList(); + // TODO: Use ParceledListSlice instead List<InputMethodInfo> getEnabledInputMethodList(); List<InputMethodSubtype> getEnabledInputMethodSubtypeList(in String imiId, boolean allowsImplicitlySelectedSubtypes); @@ -74,4 +76,7 @@ interface IInputMethodManager { boolean shouldOfferSwitchingToNextInputMethod(in IBinder token); boolean setInputMethodEnabled(String id, boolean enabled); void setAdditionalInputMethodSubtypes(String id, in InputMethodSubtype[] subtypes); + int getInputMethodWindowVisibleHeight(); + oneway void notifyTextCommitted(); + void setCursorAnchorMonitorMode(in IBinder token, int monitorMode); } diff --git a/core/java/com/android/internal/view/RotationPolicy.java b/core/java/com/android/internal/view/RotationPolicy.java index 6295314..b479cb1 100644 --- a/core/java/com/android/internal/view/RotationPolicy.java +++ b/core/java/com/android/internal/view/RotationPolicy.java @@ -18,7 +18,9 @@ package com.android.internal.view; import android.content.Context; import android.content.pm.PackageManager; +import android.content.res.Configuration; import android.database.ContentObserver; +import android.graphics.Point; import android.net.Uri; import android.os.AsyncTask; import android.os.Handler; @@ -26,15 +28,20 @@ import android.os.RemoteException; import android.os.UserHandle; import android.provider.Settings; import android.util.Log; +import android.view.Display; import android.view.IWindowManager; import android.view.Surface; import android.view.WindowManagerGlobal; +import com.android.internal.R; + /** * Provides helper functions for configuring the display rotation policy. */ public final class RotationPolicy { private static final String TAG = "RotationPolicy"; + private static final int CURRENT_ROTATION = -1; + private static final int NATURAL_ROTATION = Surface.ROTATION_0; private RotationPolicy() { } @@ -57,23 +64,33 @@ public final class RotationPolicy { } /** - * Returns true if the device supports the rotation-lock toggle feature - * in the system UI or system bar. + * Returns the orientation that will be used when locking the orientation from system UI + * with {@link #setRotationLock}. * - * When the rotation-lock toggle is supported, the "auto-rotate screen" option in - * Display settings should be hidden, but it should remain available in Accessibility - * settings. + * If the device only supports locking to its natural orientation, this will be either + * Configuration.ORIENTATION_PORTRAIT or Configuration.ORIENTATION_LANDSCAPE, + * otherwise Configuration.ORIENTATION_UNDEFINED if any orientation is lockable. */ - public static boolean isRotationLockToggleSupported(Context context) { - return isRotationSupported(context) - && context.getResources().getConfiguration().smallestScreenWidthDp >= 600; + public static int getRotationLockOrientation(Context context) { + if (!areAllRotationsAllowed(context)) { + final Point size = new Point(); + final IWindowManager wm = WindowManagerGlobal.getWindowManagerService(); + try { + wm.getInitialDisplaySize(Display.DEFAULT_DISPLAY, size); + return size.x < size.y ? + Configuration.ORIENTATION_PORTRAIT : Configuration.ORIENTATION_LANDSCAPE; + } catch (RemoteException e) { + Log.w(TAG, "Unable to get the display size"); + } + } + return Configuration.ORIENTATION_UNDEFINED; } /** - * Returns true if the rotation-lock toggle should be shown in the UI. + * Returns true if the rotation-lock toggle should be shown in system UI. */ public static boolean isRotationLockToggleVisible(Context context) { - return isRotationLockToggleSupported(context) && + return isRotationSupported(context) && Settings.System.getIntForUser(context.getContentResolver(), Settings.System.HIDE_ROTATION_LOCK_TOGGLE_FOR_ACCESSIBILITY, 0, UserHandle.USER_CURRENT) == 0; @@ -88,50 +105,42 @@ public final class RotationPolicy { } /** - * Enables or disables rotation lock. - * - * Should be used by the rotation lock toggle. + * Enables or disables rotation lock from the system UI toggle. */ public static void setRotationLock(Context context, final boolean enabled) { Settings.System.putIntForUser(context.getContentResolver(), Settings.System.HIDE_ROTATION_LOCK_TOGGLE_FOR_ACCESSIBILITY, 0, UserHandle.USER_CURRENT); - AsyncTask.execute(new Runnable() { - @Override - public void run() { - try { - IWindowManager wm = WindowManagerGlobal.getWindowManagerService(); - if (enabled) { - wm.freezeRotation(-1); - } else { - wm.thawRotation(); - } - } catch (RemoteException exc) { - Log.w(TAG, "Unable to save auto-rotate setting"); - } - } - }); + final int rotation = areAllRotationsAllowed(context) ? CURRENT_ROTATION : NATURAL_ROTATION; + setRotationLock(enabled, rotation); } /** - * Enables or disables rotation lock and adjusts whether the rotation lock toggle - * should be hidden for accessibility purposes. + * Enables or disables natural rotation lock from Accessibility settings. * - * Should be used by Display settings and Accessibility settings. + * If rotation is locked for accessibility, the system UI toggle is hidden to avoid confusion. */ public static void setRotationLockForAccessibility(Context context, final boolean enabled) { Settings.System.putIntForUser(context.getContentResolver(), Settings.System.HIDE_ROTATION_LOCK_TOGGLE_FOR_ACCESSIBILITY, enabled ? 1 : 0, UserHandle.USER_CURRENT); + setRotationLock(enabled, NATURAL_ROTATION); + } + + private static boolean areAllRotationsAllowed(Context context) { + return context.getResources().getBoolean(R.bool.config_allowAllRotations); + } + + private static void setRotationLock(final boolean enabled, final int rotation) { AsyncTask.execute(new Runnable() { @Override public void run() { try { IWindowManager wm = WindowManagerGlobal.getWindowManagerService(); if (enabled) { - wm.freezeRotation(Surface.ROTATION_0); + wm.freezeRotation(rotation); } else { wm.thawRotation(); } diff --git a/core/java/com/android/internal/view/menu/ActionMenuItem.java b/core/java/com/android/internal/view/menu/ActionMenuItem.java index 7ca6c1b..ed676bb 100644 --- a/core/java/com/android/internal/view/menu/ActionMenuItem.java +++ b/core/java/com/android/internal/view/menu/ActionMenuItem.java @@ -163,7 +163,7 @@ public class ActionMenuItem implements MenuItem { public MenuItem setIcon(int iconRes) { mIconResId = iconRes; - mIconDrawable = mContext.getResources().getDrawable(iconRes); + mIconDrawable = mContext.getDrawable(iconRes); return this; } diff --git a/core/java/com/android/internal/view/menu/ActionMenuItemView.java b/core/java/com/android/internal/view/menu/ActionMenuItemView.java index 238a9c0..891baea 100644 --- a/core/java/com/android/internal/view/menu/ActionMenuItemView.java +++ b/core/java/com/android/internal/view/menu/ActionMenuItemView.java @@ -28,8 +28,11 @@ import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.accessibility.AccessibilityEvent; +import android.widget.ActionMenuView; +import android.widget.ListPopupWindow; import android.widget.TextView; import android.widget.Toast; +import android.widget.ListPopupWindow.ForwardingListener; /** * @hide @@ -43,6 +46,8 @@ public class ActionMenuItemView extends TextView private CharSequence mTitle; private Drawable mIcon; private MenuBuilder.ItemInvoker mItemInvoker; + private ForwardingListener mForwardingListener; + private PopupCallback mPopupCallback; private boolean mAllowTextWithIcon; private boolean mExpandedFormat; @@ -60,13 +65,17 @@ public class ActionMenuItemView extends TextView this(context, attrs, 0); } - public ActionMenuItemView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public ActionMenuItemView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ActionMenuItemView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); final Resources res = context.getResources(); mAllowTextWithIcon = res.getBoolean( com.android.internal.R.bool.config_allowActionMenuItemTextWithIcon); - TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.ActionMenuItemView, 0, 0); + final TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.ActionMenuItemView, defStyleAttr, defStyleRes); mMinWidth = a.getDimensionPixelSize( com.android.internal.R.styleable.ActionMenuItemView_minWidth, 0); a.recycle(); @@ -99,6 +108,7 @@ public class ActionMenuItemView extends TextView return mItemData; } + @Override public void initialize(MenuItemImpl itemData, int menuType) { mItemData = itemData; @@ -108,8 +118,24 @@ public class ActionMenuItemView extends TextView setVisibility(itemData.isVisible() ? View.VISIBLE : View.GONE); setEnabled(itemData.isEnabled()); + + if (itemData.hasSubMenu()) { + if (mForwardingListener == null) { + mForwardingListener = new ActionMenuItemForwardingListener(); + } + } } + @Override + public boolean onTouchEvent(MotionEvent e) { + if (mItemData.hasSubMenu() && mForwardingListener != null + && mForwardingListener.onTouch(this, e)) { + return true; + } + return super.onTouchEvent(e); + } + + @Override public void onClick(View v) { if (mItemInvoker != null) { mItemInvoker.invokeItem(mItemData); @@ -120,6 +146,10 @@ public class ActionMenuItemView extends TextView mItemInvoker = invoker; } + public void setPopupCallback(PopupCallback popupCallback) { + mPopupCallback = popupCallback; + } + public boolean prefersCondensedTitle() { return true; } @@ -252,11 +282,6 @@ public class ActionMenuItemView extends TextView @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) { - // Fill all available height. - heightMeasureSpec = MeasureSpec.makeMeasureSpec( - MeasureSpec.getSize(heightMeasureSpec), MeasureSpec.EXACTLY); - } final boolean textVisible = hasText(); if (textVisible && mSavedPaddingLeft >= 0) { super.setPadding(mSavedPaddingLeft, getPaddingTop(), @@ -285,4 +310,42 @@ public class ActionMenuItemView extends TextView super.setPadding((w - dw) / 2, getPaddingTop(), getPaddingRight(), getPaddingBottom()); } } + + private class ActionMenuItemForwardingListener extends ForwardingListener { + public ActionMenuItemForwardingListener() { + super(ActionMenuItemView.this); + } + + @Override + public ListPopupWindow getPopup() { + if (mPopupCallback != null) { + return mPopupCallback.getPopup(); + } + return null; + } + + @Override + protected boolean onForwardingStarted() { + // Call the invoker, then check if the expected popup is showing. + if (mItemInvoker != null && mItemInvoker.invokeItem(mItemData)) { + final ListPopupWindow popup = getPopup(); + return popup != null && popup.isShowing(); + } + return false; + } + + @Override + protected boolean onForwardingStopped() { + final ListPopupWindow popup = getPopup(); + if (popup != null) { + popup.dismiss(); + return true; + } + return false; + } + } + + public static abstract class PopupCallback { + public abstract ListPopupWindow getPopup(); + } } diff --git a/core/java/com/android/internal/view/menu/IconMenuItemView.java b/core/java/com/android/internal/view/menu/IconMenuItemView.java index 5d0b25f..de5e279 100644 --- a/core/java/com/android/internal/view/menu/IconMenuItemView.java +++ b/core/java/com/android/internal/view/menu/IconMenuItemView.java @@ -57,8 +57,8 @@ public final class IconMenuItemView extends TextView implements MenuView.ItemVie private static String sPrependShortcutLabel; - public IconMenuItemView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs); + public IconMenuItemView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); if (sPrependShortcutLabel == null) { /* @@ -68,10 +68,9 @@ public final class IconMenuItemView extends TextView implements MenuView.ItemVie sPrependShortcutLabel = getResources().getString( com.android.internal.R.string.prepend_shortcut_label); } - - TypedArray a = - context.obtainStyledAttributes( - attrs, com.android.internal.R.styleable.MenuView, defStyle, 0); + + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.MenuView, defStyleAttr, defStyleRes); mDisabledAlpha = a.getFloat( com.android.internal.R.styleable.MenuView_itemIconDisabledAlpha, 0.8f); @@ -81,7 +80,11 @@ public final class IconMenuItemView extends TextView implements MenuView.ItemVie a.recycle(); } - + + public IconMenuItemView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + public IconMenuItemView(Context context, AttributeSet attrs) { this(context, attrs, 0); } diff --git a/core/java/com/android/internal/view/menu/ListMenuItemView.java b/core/java/com/android/internal/view/menu/ListMenuItemView.java index a2a4acc..692bdac 100644 --- a/core/java/com/android/internal/view/menu/ListMenuItemView.java +++ b/core/java/com/android/internal/view/menu/ListMenuItemView.java @@ -55,13 +55,13 @@ public class ListMenuItemView extends LinearLayout implements MenuView.ItemView private boolean mForceShowIcon; - public ListMenuItemView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs); - - TypedArray a = - context.obtainStyledAttributes( - attrs, com.android.internal.R.styleable.MenuView, defStyle, 0); - + public ListMenuItemView( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + final TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.MenuView, defStyleAttr, defStyleRes); + mBackground = a.getDrawable(com.android.internal.R.styleable.MenuView_itemBackground); mTextAppearance = a.getResourceId(com.android.internal.R.styleable. MenuView_itemTextAppearance, -1); @@ -72,6 +72,10 @@ public class ListMenuItemView extends LinearLayout implements MenuView.ItemView a.recycle(); } + public ListMenuItemView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + public ListMenuItemView(Context context, AttributeSet attrs) { this(context, attrs, 0); } diff --git a/core/java/com/android/internal/view/menu/ListMenuPresenter.java b/core/java/com/android/internal/view/menu/ListMenuPresenter.java index e1bb3621..c476354 100644 --- a/core/java/com/android/internal/view/menu/ListMenuPresenter.java +++ b/core/java/com/android/internal/view/menu/ListMenuPresenter.java @@ -17,7 +17,6 @@ package com.android.internal.view.menu; import android.content.Context; -import android.database.DataSetObserver; import android.os.Bundle; import android.os.Parcelable; import android.util.SparseArray; diff --git a/core/java/com/android/internal/view/menu/MenuBuilder.java b/core/java/com/android/internal/view/menu/MenuBuilder.java index 5464284..5d7d322 100644 --- a/core/java/com/android/internal/view/menu/MenuBuilder.java +++ b/core/java/com/android/internal/view/menu/MenuBuilder.java @@ -926,7 +926,7 @@ public class MenuBuilder implements Menu { * sub menu is about to be shown, <var>allMenusAreClosing</var> * is false. */ - final void close(boolean allMenusAreClosing) { + public final void close(boolean allMenusAreClosing) { if (mIsClosing) return; mIsClosing = true; @@ -953,7 +953,7 @@ public class MenuBuilder implements Menu { * false if only item properties changed. * (Visibility is a structural property since it affects layout.) */ - void onItemsChanged(boolean structureChanged) { + public void onItemsChanged(boolean structureChanged) { if (!mPreventDispatchingItemsChanged) { if (structureChanged) { mIsVisibleItemsStale = true; @@ -1007,7 +1007,7 @@ public class MenuBuilder implements Menu { onItemsChanged(true); } - ArrayList<MenuItemImpl> getVisibleItems() { + public ArrayList<MenuItemImpl> getVisibleItems() { if (!mIsVisibleItemsStale) return mVisibleItems; // Refresh the visible items @@ -1092,12 +1092,12 @@ public class MenuBuilder implements Menu { mIsActionItemsStale = false; } - ArrayList<MenuItemImpl> getActionItems() { + public ArrayList<MenuItemImpl> getActionItems() { flagActionItems(); return mActionItems; } - ArrayList<MenuItemImpl> getNonActionItems() { + public ArrayList<MenuItemImpl> getNonActionItems() { flagActionItems(); return mNonActionItems; } @@ -1128,7 +1128,7 @@ public class MenuBuilder implements Menu { } if (iconRes > 0) { - mHeaderIcon = r.getDrawable(iconRes); + mHeaderIcon = getContext().getDrawable(iconRes); } else if (icon != null) { mHeaderIcon = icon; } diff --git a/core/java/com/android/internal/view/menu/MenuItemImpl.java b/core/java/com/android/internal/view/menu/MenuItemImpl.java index 4d0a326..61dcaca 100644 --- a/core/java/com/android/internal/view/menu/MenuItemImpl.java +++ b/core/java/com/android/internal/view/menu/MenuItemImpl.java @@ -385,7 +385,7 @@ public final class MenuItemImpl implements MenuItem { } if (mIconResId != NO_ICON) { - Drawable icon = mMenu.getResources().getDrawable(mIconResId); + Drawable icon = mMenu.getContext().getDrawable(mIconResId); mIconResId = NO_ICON; mIconDrawable = icon; return icon; diff --git a/core/java/com/android/internal/view/menu/MenuPopupHelper.java b/core/java/com/android/internal/view/menu/MenuPopupHelper.java index 05e9a66..d664058 100644 --- a/core/java/com/android/internal/view/menu/MenuPopupHelper.java +++ b/core/java/com/android/internal/view/menu/MenuPopupHelper.java @@ -23,7 +23,6 @@ import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MenuItem; -import android.view.MotionEvent; import android.view.View; import android.view.View.MeasureSpec; import android.view.ViewGroup; diff --git a/core/java/com/android/internal/widget/AbsActionBarView.java b/core/java/com/android/internal/widget/AbsActionBarView.java index f3891c7..183478f 100644 --- a/core/java/com/android/internal/widget/AbsActionBarView.java +++ b/core/java/com/android/internal/widget/AbsActionBarView.java @@ -16,8 +16,8 @@ package com.android.internal.widget; import com.android.internal.R; -import com.android.internal.view.menu.ActionMenuPresenter; -import com.android.internal.view.menu.ActionMenuView; +import android.widget.ActionMenuPresenter; +import android.widget.ActionMenuView; import android.animation.Animator; import android.animation.AnimatorSet; @@ -47,15 +47,20 @@ public abstract class AbsActionBarView extends ViewGroup { private static final int FADE_DURATION = 200; public AbsActionBarView(Context context) { - super(context); + this(context, null); } public AbsActionBarView(Context context, AttributeSet attrs) { - super(context, attrs); + this(context, attrs, 0); } - public AbsActionBarView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public AbsActionBarView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public AbsActionBarView( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); } @Override @@ -95,9 +100,6 @@ public abstract class AbsActionBarView extends ViewGroup { public void setContentHeight(int height) { mContentHeight = height; - if (mMenuView != null) { - mMenuView.setMaxItemHeight(mContentHeight); - } requestLayout(); } diff --git a/core/java/com/android/internal/widget/ActionBarContainer.java b/core/java/com/android/internal/widget/ActionBarContainer.java index 8a49899..ed07514 100644 --- a/core/java/com/android/internal/widget/ActionBarContainer.java +++ b/core/java/com/android/internal/widget/ActionBarContainer.java @@ -16,10 +16,10 @@ package com.android.internal.widget; -import android.app.ActionBar; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; +import android.graphics.ColorFilter; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.ActionMode; @@ -43,6 +43,7 @@ public class ActionBarContainer extends FrameLayout { private Drawable mSplitBackground; private boolean mIsSplit; private boolean mIsStacked; + private int mHeight; public ActionBarContainer(Context context) { this(context, null); @@ -51,13 +52,15 @@ public class ActionBarContainer extends FrameLayout { public ActionBarContainer(Context context, AttributeSet attrs) { super(context, attrs); - setBackgroundDrawable(null); + // Set a transparent background so that we project appropriately. + setBackground(new ActionBarBackgroundDrawable()); TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.ActionBar); mBackground = a.getDrawable(com.android.internal.R.styleable.ActionBar_background); mStackedBackground = a.getDrawable( com.android.internal.R.styleable.ActionBar_backgroundStacked); + mHeight = a.getDimensionPixelSize(com.android.internal.R.styleable.ActionBar_height, -1); if (getId() == com.android.internal.R.id.split_action_bar) { mIsSplit = true; @@ -243,24 +246,6 @@ public class ActionBarContainer extends FrameLayout { } @Override - public void onDraw(Canvas canvas) { - if (getWidth() == 0 || getHeight() == 0) { - return; - } - - if (mIsSplit) { - if (mSplitBackground != null) mSplitBackground.draw(canvas); - } else { - if (mBackground != null) { - mBackground.draw(canvas); - } - if (mStackedBackground != null && mIsStacked) { - mStackedBackground.draw(canvas); - } - } - } - - @Override public ActionMode startActionModeForChild(View child, ActionMode.Callback callback) { // No starting an action mode for an action bar child! (Where would it go?) return null; @@ -268,6 +253,11 @@ public class ActionBarContainer extends FrameLayout { @Override public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (mActionBarView == null && + MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST && mHeight >= 0) { + heightMeasureSpec = MeasureSpec.makeMeasureSpec( + Math.min(mHeight, MeasureSpec.getSize(heightMeasureSpec)), MeasureSpec.AT_MOST); + } super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (mActionBarView == null) return; @@ -291,12 +281,13 @@ public class ActionBarContainer extends FrameLayout { public void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); - final boolean hasTabs = mTabContainer != null && mTabContainer.getVisibility() != GONE; + final View tabContainer = mTabContainer; + final boolean hasTabs = tabContainer != null && tabContainer.getVisibility() != GONE; - if (mTabContainer != null && mTabContainer.getVisibility() != GONE) { + if (tabContainer != null && tabContainer.getVisibility() != GONE) { final int containerHeight = getMeasuredHeight(); - final int tabHeight = mTabContainer.getMeasuredHeight(); - mTabContainer.layout(l, containerHeight - tabHeight, r, containerHeight); + final int tabHeight = tabContainer.getMeasuredHeight(); + tabContainer.layout(l, containerHeight - tabHeight, r, containerHeight); } boolean needsInvalidate = false; @@ -307,13 +298,15 @@ public class ActionBarContainer extends FrameLayout { } } else { if (mBackground != null) { - mBackground.setBounds(mActionBarView.getLeft(), mActionBarView.getTop(), - mActionBarView.getRight(), mActionBarView.getBottom()); + final ActionBarView actionBarView = mActionBarView; + mBackground.setBounds(actionBarView.getLeft(), actionBarView.getTop(), + actionBarView.getRight(), actionBarView.getBottom()); needsInvalidate = true; } - if ((mIsStacked = hasTabs && mStackedBackground != null)) { - mStackedBackground.setBounds(mTabContainer.getLeft(), mTabContainer.getTop(), - mTabContainer.getRight(), mTabContainer.getBottom()); + mIsStacked = hasTabs; + if (hasTabs && mStackedBackground != null) { + mStackedBackground.setBounds(tabContainer.getLeft(), tabContainer.getTop(), + tabContainer.getRight(), tabContainer.getBottom()); needsInvalidate = true; } } @@ -322,4 +315,37 @@ public class ActionBarContainer extends FrameLayout { invalidate(); } } + + /** + * Dummy drawable so that we don't break background display lists and + * projection surfaces. + */ + private class ActionBarBackgroundDrawable extends Drawable { + @Override + public void draw(Canvas canvas) { + if (mIsSplit) { + if (mSplitBackground != null) mSplitBackground.draw(canvas); + } else { + if (mBackground != null) { + mBackground.draw(canvas); + } + if (mStackedBackground != null && mIsStacked) { + mStackedBackground.draw(canvas); + } + } + } + + @Override + public void setAlpha(int alpha) { + } + + @Override + public void setColorFilter(ColorFilter cf) { + } + + @Override + public int getOpacity() { + return 0; + } + } } diff --git a/core/java/com/android/internal/widget/ActionBarContextView.java b/core/java/com/android/internal/widget/ActionBarContextView.java index 8bc1081..e10070f 100644 --- a/core/java/com/android/internal/widget/ActionBarContextView.java +++ b/core/java/com/android/internal/widget/ActionBarContextView.java @@ -16,8 +16,8 @@ package com.android.internal.widget; import com.android.internal.R; -import com.android.internal.view.menu.ActionMenuPresenter; -import com.android.internal.view.menu.ActionMenuView; +import android.widget.ActionMenuPresenter; +import android.widget.ActionMenuView; import com.android.internal.view.menu.MenuBuilder; import android.animation.Animator; @@ -25,7 +25,6 @@ import android.animation.Animator.AnimatorListener; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.content.Context; -import android.content.res.Configuration; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.text.TextUtils; @@ -74,10 +73,16 @@ public class ActionBarContextView extends AbsActionBarView implements AnimatorLi this(context, attrs, com.android.internal.R.attr.actionModeStyle); } - public ActionBarContextView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ActionMode, defStyle, 0); + public ActionBarContextView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ActionBarContextView( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + final TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.ActionMode, defStyleAttr, defStyleRes); setBackgroundDrawable(a.getDrawable( com.android.internal.R.styleable.ActionMode_background)); mTitleStyleRes = a.getResourceId( diff --git a/core/java/com/android/internal/widget/ActionBarOverlayLayout.java b/core/java/com/android/internal/widget/ActionBarOverlayLayout.java index c957b67..01bee0c 100644 --- a/core/java/com/android/internal/widget/ActionBarOverlayLayout.java +++ b/core/java/com/android/internal/widget/ActionBarOverlayLayout.java @@ -16,18 +16,22 @@ package com.android.internal.widget; -import android.graphics.Canvas; -import android.graphics.drawable.Drawable; -import android.os.Build; -import android.view.ViewGroup; -import android.view.WindowInsets; -import com.android.internal.app.ActionBarImpl; - +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.content.Context; import android.content.res.TypedArray; +import android.graphics.Canvas; import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; import android.util.AttributeSet; +import android.util.IntProperty; +import android.util.Property; import android.view.View; +import android.view.ViewGroup; +import android.view.ViewPropertyAnimator; +import android.view.WindowInsets; +import android.widget.OverScroller; /** * Special layout for the containing of an overlay action bar (and its @@ -38,7 +42,7 @@ public class ActionBarOverlayLayout extends ViewGroup { private static final String TAG = "ActionBarOverlayLayout"; private int mActionBarHeight; - private ActionBarImpl mActionBar; + //private WindowDecorActionBar mActionBar; private int mWindowVisibility = View.VISIBLE; // The main UI elements that we handle the layout of. @@ -54,6 +58,10 @@ public class ActionBarOverlayLayout extends ViewGroup { private boolean mIgnoreWindowContentOverlay; private boolean mOverlayMode; + private boolean mHasNonEmbeddedTabs; + private boolean mHideOnContentScroll; + private boolean mAnimatingForFling; + private int mHideOnContentScrollReference; private int mLastSystemUiVisibility; private final Rect mBaseContentInsets = new Rect(); private final Rect mLastBaseContentInsets = new Rect(); @@ -62,6 +70,84 @@ public class ActionBarOverlayLayout extends ViewGroup { private final Rect mInnerInsets = new Rect(); private final Rect mLastInnerInsets = new Rect(); + private ActionBarVisibilityCallback mActionBarVisibilityCallback; + + private final int ACTION_BAR_ANIMATE_DELAY = 600; // ms + + private OverScroller mFlingEstimator; + + private ViewPropertyAnimator mCurrentActionBarTopAnimator; + private ViewPropertyAnimator mCurrentActionBarBottomAnimator; + + private final Animator.AnimatorListener mTopAnimatorListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mCurrentActionBarTopAnimator = null; + mAnimatingForFling = false; + } + + @Override + public void onAnimationCancel(Animator animation) { + mCurrentActionBarTopAnimator = null; + mAnimatingForFling = false; + } + }; + + private final Animator.AnimatorListener mBottomAnimatorListener = + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mCurrentActionBarBottomAnimator = null; + mAnimatingForFling = false; + } + + @Override + public void onAnimationCancel(Animator animation) { + mCurrentActionBarBottomAnimator = null; + mAnimatingForFling = false; + } + }; + + private final Runnable mRemoveActionBarHideOffset = new Runnable() { + public void run() { + haltActionBarHideOffsetAnimations(); + mCurrentActionBarTopAnimator = mActionBarTop.animate().translationY(0) + .setListener(mTopAnimatorListener); + if (mActionBarBottom != null && mActionBarBottom.getVisibility() != GONE) { + mCurrentActionBarBottomAnimator = mActionBarBottom.animate().translationY(0) + .setListener(mBottomAnimatorListener); + } + } + }; + + private final Runnable mAddActionBarHideOffset = new Runnable() { + public void run() { + haltActionBarHideOffsetAnimations(); + mCurrentActionBarTopAnimator = mActionBarTop.animate() + .translationY(-mActionBarTop.getHeight()) + .setListener(mTopAnimatorListener); + if (mActionBarBottom != null && mActionBarBottom.getVisibility() != GONE) { + mCurrentActionBarBottomAnimator = mActionBarBottom.animate() + .translationY(mActionBarBottom.getHeight()) + .setListener(mBottomAnimatorListener); + } + } + }; + + public static final Property<ActionBarOverlayLayout, Integer> ACTION_BAR_HIDE_OFFSET = + new IntProperty<ActionBarOverlayLayout>("actionBarHideOffset") { + + @Override + public void setValue(ActionBarOverlayLayout object, int value) { + object.setActionBarHideOffset(value); + } + + @Override + public Integer get(ActionBarOverlayLayout object) { + return object.getActionBarHideOffset(); + } + }; + static final int[] ATTRS = new int [] { com.android.internal.R.attr.actionBarSize, com.android.internal.R.attr.windowContentOverlay @@ -86,14 +172,22 @@ public class ActionBarOverlayLayout extends ViewGroup { mIgnoreWindowContentOverlay = context.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.KITKAT; + + mFlingEstimator = new OverScroller(context); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + haltActionBarHideOffsetAnimations(); } - public void setActionBar(ActionBarImpl impl) { - mActionBar = impl; + public void setActionBarVisibilityCallback(ActionBarVisibilityCallback cb) { + mActionBarVisibilityCallback = cb; if (getWindowToken() != null) { // This is being initialized after being added to a window; // make sure to update all state now. - mActionBar.setWindowVisibility(mWindowVisibility); + mActionBarVisibilityCallback.onWindowVisibilityChanged(mWindowVisibility); if (mLastSystemUiVisibility != 0) { int newVis = mLastSystemUiVisibility; onWindowSystemUiVisibilityChanged(newVis); @@ -114,6 +208,14 @@ public class ActionBarOverlayLayout extends ViewGroup { Build.VERSION_CODES.KITKAT; } + public boolean isInOverlayMode() { + return mOverlayMode; + } + + public void setHasNonEmbeddedTabs(boolean hasNonEmbeddedTabs) { + mHasNonEmbeddedTabs = hasNonEmbeddedTabs; + } + public void setShowingForActionMode(boolean showing) { if (showing) { // Here's a fun hack: if the status bar is currently being hidden, @@ -140,19 +242,18 @@ public class ActionBarOverlayLayout extends ViewGroup { pullChildren(); final int diff = mLastSystemUiVisibility ^ visible; mLastSystemUiVisibility = visible; - final boolean barVisible = (visible&SYSTEM_UI_FLAG_FULLSCREEN) == 0; - final boolean wasVisible = mActionBar != null ? mActionBar.isSystemShowing() : true; - final boolean stable = (visible&SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0; - if (mActionBar != null) { + final boolean barVisible = (visible & SYSTEM_UI_FLAG_FULLSCREEN) == 0; + final boolean stable = (visible & SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0; + if (mActionBarVisibilityCallback != null) { // We want the bar to be visible if it is not being hidden, // or the app has not turned on a stable UI mode (meaning they // are performing explicit layout around the action bar). - mActionBar.enableContentAnimations(!stable); - if (barVisible || !stable) mActionBar.showForSystem(); - else mActionBar.hideForSystem(); + mActionBarVisibilityCallback.enableContentAnimations(!stable); + if (barVisible || !stable) mActionBarVisibilityCallback.showForSystem(); + else mActionBarVisibilityCallback.hideForSystem(); } - if ((diff&SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0) { - if (mActionBar != null) { + if ((diff & SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0) { + if (mActionBarVisibilityCallback != null) { requestApplyInsets(); } } @@ -162,8 +263,8 @@ public class ActionBarOverlayLayout extends ViewGroup { protected void onWindowVisibilityChanged(int visibility) { super.onWindowVisibilityChanged(visibility); mWindowVisibility = visibility; - if (mActionBar != null) { - mActionBar.setWindowVisibility(visibility); + if (mActionBarVisibilityCallback != null) { + mActionBarVisibilityCallback.onWindowVisibilityChanged(visibility); } } @@ -279,14 +380,14 @@ public class ActionBarOverlayLayout extends ViewGroup { // This is the standard space needed for the action bar. For stable measurement, // we can't depend on the size currently reported by it -- this must remain constant. topInset = mActionBarHeight; - if (mActionBar != null && mActionBar.hasNonEmbeddedTabs()) { - View tabs = mActionBarTop.getTabContainer(); + if (mHasNonEmbeddedTabs) { + final View tabs = mActionBarTop.getTabContainer(); if (tabs != null) { // If tabs are not embedded, increase space on top to account for them. topInset += mActionBarHeight; } } - } else if (mActionBarTop.getVisibility() == VISIBLE) { + } else if (mActionBarTop.getVisibility() != GONE) { // This is the space needed on top of the window for all of the action bar // and tabs. topInset = mActionBarTop.getMeasuredHeight(); @@ -395,16 +496,138 @@ public class ActionBarOverlayLayout extends ViewGroup { return false; } + @Override + public boolean onStartNestedScroll(View child, View target, int axes) { + if ((axes & SCROLL_AXIS_VERTICAL) == 0 || mActionBarTop.getVisibility() != VISIBLE) { + return false; + } + return mHideOnContentScroll; + } + + @Override + public void onNestedScrollAccepted(View child, View target, int axes) { + super.onNestedScrollAccepted(child, target, axes); + mHideOnContentScrollReference = getActionBarHideOffset(); + haltActionBarHideOffsetAnimations(); + if (mActionBarVisibilityCallback != null) { + mActionBarVisibilityCallback.onContentScrollStarted(); + } + } + + @Override + public void onNestedScroll(View target, int dxConsumed, int dyConsumed, + int dxUnconsumed, int dyUnconsumed) { + mHideOnContentScrollReference += dyConsumed; + setActionBarHideOffset(mHideOnContentScrollReference); + } + + @Override + public void onStopNestedScroll(View target) { + super.onStopNestedScroll(target); + if (mHideOnContentScroll && !mAnimatingForFling) { + if (mHideOnContentScrollReference <= mActionBarTop.getHeight()) { + postRemoveActionBarHideOffset(); + } else { + postAddActionBarHideOffset(); + } + } + if (mActionBarVisibilityCallback != null) { + mActionBarVisibilityCallback.onContentScrollStopped(); + } + } + + @Override + public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { + if (!mHideOnContentScroll || !consumed) { + return false; + } + if (shouldHideActionBarOnFling(velocityX, velocityY)) { + addActionBarHideOffset(); + } else { + removeActionBarHideOffset(); + } + mAnimatingForFling = true; + return true; + } + void pullChildren() { if (mContent == null) { mContent = findViewById(com.android.internal.R.id.content); - mActionBarTop = (ActionBarContainer)findViewById( + mActionBarTop = (ActionBarContainer) findViewById( com.android.internal.R.id.action_bar_container); mActionBarView = (ActionBarView) findViewById(com.android.internal.R.id.action_bar); mActionBarBottom = findViewById(com.android.internal.R.id.split_action_bar); } } + public void setHideOnContentScrollEnabled(boolean hideOnContentScroll) { + if (hideOnContentScroll != mHideOnContentScroll) { + mHideOnContentScroll = hideOnContentScroll; + if (!hideOnContentScroll) { + stopNestedScroll(); + haltActionBarHideOffsetAnimations(); + setActionBarHideOffset(0); + } + } + } + + public boolean isHideOnContentScrollEnabled() { + return mHideOnContentScroll; + } + + public int getActionBarHideOffset() { + return -((int) mActionBarTop.getTranslationY()); + } + + public void setActionBarHideOffset(int offset) { + haltActionBarHideOffsetAnimations(); + final int topHeight = mActionBarTop.getHeight(); + offset = Math.max(0, Math.min(offset, topHeight)); + mActionBarTop.setTranslationY(-offset); + if (mActionBarBottom != null && mActionBarBottom.getVisibility() != GONE) { + // Match the hide offset proportionally for a split bar + final float fOffset = (float) offset / topHeight; + final int bOffset = (int) (mActionBarBottom.getHeight() * fOffset); + mActionBarBottom.setTranslationY(bOffset); + } + } + + private void haltActionBarHideOffsetAnimations() { + removeCallbacks(mRemoveActionBarHideOffset); + removeCallbacks(mAddActionBarHideOffset); + if (mCurrentActionBarTopAnimator != null) { + mCurrentActionBarTopAnimator.cancel(); + } + if (mCurrentActionBarBottomAnimator != null) { + mCurrentActionBarBottomAnimator.cancel(); + } + } + + private void postRemoveActionBarHideOffset() { + haltActionBarHideOffsetAnimations(); + postDelayed(mRemoveActionBarHideOffset, ACTION_BAR_ANIMATE_DELAY); + } + + private void postAddActionBarHideOffset() { + haltActionBarHideOffsetAnimations(); + postDelayed(mAddActionBarHideOffset, ACTION_BAR_ANIMATE_DELAY); + } + + private void removeActionBarHideOffset() { + haltActionBarHideOffsetAnimations(); + mRemoveActionBarHideOffset.run(); + } + + private void addActionBarHideOffset() { + haltActionBarHideOffsetAnimations(); + mAddActionBarHideOffset.run(); + } + + private boolean shouldHideActionBarOnFling(float velocityX, float velocityY) { + mFlingEstimator.fling(0, 0, 0, (int) velocityY, 0, 0, Integer.MIN_VALUE, Integer.MAX_VALUE); + final int finalY = mFlingEstimator.getFinalY(); + return finalY > mActionBarTop.getHeight(); + } public static class LayoutParams extends MarginLayoutParams { public LayoutParams(Context c, AttributeSet attrs) { @@ -423,4 +646,13 @@ public class ActionBarOverlayLayout extends ViewGroup { super(source); } } + + public interface ActionBarVisibilityCallback { + void onWindowVisibilityChanged(int visibility); + void showForSystem(); + void hideForSystem(); + void enableContentAnimations(boolean enable); + void onContentScrollStarted(); + void onContentScrollStopped(); + } } diff --git a/core/java/com/android/internal/widget/ActionBarView.java b/core/java/com/android/internal/widget/ActionBarView.java index 786f5cf..60631b9 100644 --- a/core/java/com/android/internal/widget/ActionBarView.java +++ b/core/java/com/android/internal/widget/ActionBarView.java @@ -41,6 +41,8 @@ import android.view.ViewGroup; import android.view.ViewParent; import android.view.Window; import android.view.accessibility.AccessibilityEvent; +import android.widget.ActionMenuPresenter; +import android.widget.ActionMenuView; import android.widget.AdapterView; import android.widget.FrameLayout; import android.widget.ImageView; @@ -52,8 +54,6 @@ import android.widget.TextView; import com.android.internal.R; import com.android.internal.transition.ActionBarTransition; import com.android.internal.view.menu.ActionMenuItem; -import com.android.internal.view.menu.ActionMenuPresenter; -import com.android.internal.view.menu.ActionMenuView; import com.android.internal.view.menu.MenuBuilder; import com.android.internal.view.menu.MenuItemImpl; import com.android.internal.view.menu.MenuPresenter; @@ -430,6 +430,7 @@ public class ActionBarView extends AbsActionBarView { mActionMenuPresenter.setItemLimit(Integer.MAX_VALUE); // Span the whole width layoutParams.width = LayoutParams.MATCH_PARENT; + layoutParams.height = LayoutParams.WRAP_CONTENT; configPresenters(builder); menuView = (ActionMenuView) mActionMenuPresenter.getMenuView(this); if (mSplitView != null) { @@ -696,7 +697,7 @@ public class ActionBarView extends AbsActionBarView { } public void setIcon(int resId) { - setIcon(resId != 0 ? mContext.getResources().getDrawable(resId) : null); + setIcon(resId != 0 ? mContext.getDrawable(resId) : null); } public boolean hasIcon() { @@ -711,7 +712,7 @@ public class ActionBarView extends AbsActionBarView { } public void setLogo(int resId) { - setLogo(resId != 0 ? mContext.getResources().getDrawable(resId) : null); + setLogo(resId != 0 ? mContext.getDrawable(resId) : null); } public boolean hasLogo() { @@ -1416,7 +1417,7 @@ public class ActionBarView extends AbsActionBarView { public void setUpIndicator(int resId) { mUpIndicatorRes = resId; - mUpView.setImageDrawable(resId != 0 ? getResources().getDrawable(resId) : null); + mUpView.setImageDrawable(resId != 0 ? getContext().getDrawable(resId) : null); } @Override diff --git a/core/java/com/android/internal/widget/AutoScrollHelper.java b/core/java/com/android/internal/widget/AutoScrollHelper.java index 7a294aa..0d468ca 100644 --- a/core/java/com/android/internal/widget/AutoScrollHelper.java +++ b/core/java/com/android/internal/widget/AutoScrollHelper.java @@ -892,6 +892,10 @@ public abstract class AutoScrollHelper implements View.OnTouchListener { public boolean canTargetScrollVertically(int direction) { final AbsListView target = mTarget; final int itemCount = target.getCount(); + if (itemCount == 0) { + return false; + } + final int childCount = target.getChildCount(); final int firstPosition = target.getFirstVisiblePosition(); final int lastPosition = firstPosition + childCount; diff --git a/core/java/com/android/internal/widget/DialogTitle.java b/core/java/com/android/internal/widget/DialogTitle.java index b86c438..7ea3d6b 100644 --- a/core/java/com/android/internal/widget/DialogTitle.java +++ b/core/java/com/android/internal/widget/DialogTitle.java @@ -28,10 +28,13 @@ import android.widget.TextView; * the text to the available space. */ public class DialogTitle extends TextView { - - public DialogTitle(Context context, AttributeSet attrs, - int defStyle) { - super(context, attrs, defStyle); + + public DialogTitle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public DialogTitle(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); } public DialogTitle(Context context, AttributeSet attrs) { diff --git a/core/java/com/android/internal/widget/FaceUnlockView.java b/core/java/com/android/internal/widget/FaceUnlockView.java index e3c1247..121e601 100644 --- a/core/java/com/android/internal/widget/FaceUnlockView.java +++ b/core/java/com/android/internal/widget/FaceUnlockView.java @@ -18,8 +18,6 @@ package com.android.internal.widget; import android.content.Context; import android.util.AttributeSet; -import android.util.Log; -import android.view.View; import android.widget.RelativeLayout; public class FaceUnlockView extends RelativeLayout { diff --git a/core/java/com/android/internal/widget/ILockSettings.aidl b/core/java/com/android/internal/widget/ILockSettings.aidl index 91056f1..9501f92 100644 --- a/core/java/com/android/internal/widget/ILockSettings.aidl +++ b/core/java/com/android/internal/widget/ILockSettings.aidl @@ -28,6 +28,7 @@ interface ILockSettings { boolean checkPattern(in String pattern, int userId); void setLockPassword(in String password, int userId); boolean checkPassword(in String password, int userId); + boolean checkVoldPassword(int userId); boolean havePattern(int userId); boolean havePassword(int userId); void removeUser(int userId); diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index 8602260..2882b54 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -19,18 +19,21 @@ package com.android.internal.widget; import android.Manifest; import android.app.ActivityManagerNative; import android.app.admin.DevicePolicyManager; +import android.app.trust.TrustManager; import android.appwidget.AppWidgetManager; +import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; -import android.os.Binder; +import android.net.Uri; import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; import android.os.UserHandle; import android.os.storage.IMountService; +import android.os.storage.StorageManager; import android.provider.Settings; import android.telephony.TelephonyManager; import android.text.TextUtils; @@ -46,6 +49,8 @@ import com.google.android.collect.Lists; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; /** @@ -145,6 +150,8 @@ public class LockPatternUtils { private static final String LOCK_SCREEN_OWNER_INFO_ENABLED = Settings.Secure.LOCK_SCREEN_OWNER_INFO_ENABLED; + private static final String ENABLED_TRUST_AGENTS = "lockscreen.enabledtrustagents"; + private final Context mContext; private final ContentResolver mContentResolver; private DevicePolicyManager mDevicePolicyManager; @@ -167,6 +174,15 @@ public class LockPatternUtils { return mDevicePolicyManager; } + private TrustManager getTrustManager() { + TrustManager trust = (TrustManager) mContext.getSystemService(Context.TRUST_SERVICE); + if (trust == null) { + Log.e(TAG, "Can't get TrustManagerService: is it running?", + new IllegalStateException("Stack trace:")); + } + return trust; + } + /** * @param contentResolver Used to look up and save settings. */ @@ -242,10 +258,14 @@ public class LockPatternUtils { */ public void reportFailedPasswordAttempt() { getDevicePolicyManager().reportFailedPasswordAttempt(getCurrentOrCallingUserId()); + getTrustManager().reportUnlockAttempt(false /* authenticated */, + getCurrentOrCallingUserId()); } public void reportSuccessfulPasswordAttempt() { getDevicePolicyManager().reportSuccessfulPasswordAttempt(getCurrentOrCallingUserId()); + getTrustManager().reportUnlockAttempt(true /* authenticated */, + getCurrentOrCallingUserId()); } public void setCurrentUser(int userId) { @@ -313,6 +333,20 @@ public class LockPatternUtils { } /** + * Check to see if vold already has the password. + * Note that this also clears vold's copy of the password. + * @return Whether the vold password matches or not. + */ + public boolean checkVoldPassword() { + final int userId = getCurrentOrCallingUserId(); + try { + return getLockSettings().checkVoldPassword(userId); + } catch (RemoteException re) { + return false; + } + } + + /** * Check to see if a password matches any of the passwords stored in the * password history. * @@ -496,38 +530,71 @@ public class LockPatternUtils { */ public void saveLockPattern(List<LockPatternView.Cell> pattern, boolean isFallback) { try { - getLockSettings().setLockPattern(patternToString(pattern), getCurrentOrCallingUserId()); + int userId = getCurrentOrCallingUserId(); + getLockSettings().setLockPattern(patternToString(pattern), userId); DevicePolicyManager dpm = getDevicePolicyManager(); if (pattern != null) { + + int userHandle = userId; + if (userHandle == UserHandle.USER_OWNER) { + String stringPattern = patternToString(pattern); + updateEncryptionPassword(StorageManager.CRYPT_TYPE_PATTERN, stringPattern); + } + setBoolean(PATTERN_EVER_CHOSEN_KEY, true); if (!isFallback) { deleteGallery(); setLong(PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_SOMETHING); dpm.setActivePasswordState(DevicePolicyManager.PASSWORD_QUALITY_SOMETHING, - pattern.size(), 0, 0, 0, 0, 0, 0, getCurrentOrCallingUserId()); + pattern.size(), 0, 0, 0, 0, 0, 0, userId); } else { setLong(PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_BIOMETRIC_WEAK); setLong(PASSWORD_TYPE_ALTERNATE_KEY, DevicePolicyManager.PASSWORD_QUALITY_SOMETHING); finishBiometricWeak(); dpm.setActivePasswordState(DevicePolicyManager.PASSWORD_QUALITY_BIOMETRIC_WEAK, - 0, 0, 0, 0, 0, 0, 0, getCurrentOrCallingUserId()); + 0, 0, 0, 0, 0, 0, 0, userId); } } else { dpm.setActivePasswordState(DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, 0, 0, - 0, 0, 0, 0, 0, getCurrentOrCallingUserId()); + 0, 0, 0, 0, 0, userId); } } catch (RemoteException re) { Log.e(TAG, "Couldn't save lock pattern " + re); } } + private void updateCryptoUserInfo() { + int userId = getCurrentOrCallingUserId(); + if (userId != UserHandle.USER_OWNER) { + return; + } + + final String ownerInfo = isOwnerInfoEnabled() ? getOwnerInfo(userId) : ""; + + IBinder service = ServiceManager.getService("mount"); + if (service == null) { + Log.e(TAG, "Could not find the mount service to update the user info"); + return; + } + + IMountService mountService = IMountService.Stub.asInterface(service); + try { + Log.d(TAG, "Setting owner info"); + mountService.setField("OwnerInfo", ownerInfo); + } catch (RemoteException e) { + Log.e(TAG, "Error changing user info", e); + } + } + public void setOwnerInfo(String info, int userId) { setString(LOCK_SCREEN_OWNER_INFO, info, userId); + updateCryptoUserInfo(); } public void setOwnerInfoEnabled(boolean enabled) { setBoolean(LOCK_SCREEN_OWNER_INFO_ENABLED, enabled); + updateCryptoUserInfo(); } public String getOwnerInfo(int userId) { @@ -566,7 +633,7 @@ public class LockPatternUtils { } /** Update the encryption password if it is enabled **/ - private void updateEncryptionPassword(String password) { + private void updateEncryptionPassword(int type, String password) { DevicePolicyManager dpm = getDevicePolicyManager(); if (dpm.getStorageEncryptionStatus(getCurrentOrCallingUserId()) != DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE) { @@ -581,7 +648,7 @@ public class LockPatternUtils { IMountService mountService = IMountService.Stub.asInterface(service); try { - mountService.changeEncryptionPassword(password); + mountService.changeEncryptionPassword(type, password); } catch (RemoteException e) { Log.e(TAG, "Error changing encryption password", e); } @@ -624,12 +691,15 @@ public class LockPatternUtils { getLockSettings().setLockPassword(password, userHandle); DevicePolicyManager dpm = getDevicePolicyManager(); if (password != null) { + int computedQuality = computePasswordQuality(password); + if (userHandle == UserHandle.USER_OWNER) { // Update the encryption password. - updateEncryptionPassword(password); + int type = computedQuality == DevicePolicyManager.PASSWORD_QUALITY_NUMERIC + ? StorageManager.CRYPT_TYPE_PIN : StorageManager.CRYPT_TYPE_PASSWORD; + updateEncryptionPassword(type, password); } - int computedQuality = computePasswordQuality(password); if (!isFallback) { deleteGallery(); setLong(PASSWORD_TYPE_KEY, Math.max(quality, computedQuality), userHandle); @@ -676,8 +746,7 @@ public class LockPatternUtils { 0, 0, 0, 0, 0, 0, 0, userHandle); } // Add the password to the password history. We assume all - // password - // hashes have the same length for simplicity of implementation. + // password hashes have the same length for simplicity of implementation. String passwordHistory = getString(PASSWORD_HISTORY_KEY, userHandle); if (passwordHistory == null) { passwordHistory = new String(); @@ -696,6 +765,11 @@ public class LockPatternUtils { } setString(PASSWORD_HISTORY_KEY, passwordHistory, userHandle); } else { + if (userHandle == UserHandle.USER_OWNER) { + // Update the encryption password. + updateEncryptionPassword(StorageManager.CRYPT_TYPE_DEFAULT, password); + } + dpm.setActivePasswordState( DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, 0, 0, 0, 0, 0, 0, 0, userHandle); @@ -714,13 +788,13 @@ public class LockPatternUtils { */ public int getKeyguardStoredPasswordQuality() { int quality = - (int) getLong(PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_SOMETHING); + (int) getLong(PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED); // If the user has chosen to use weak biometric sensor, then return the backup locking // method and treat biometric as a special case. if (quality == DevicePolicyManager.PASSWORD_QUALITY_BIOMETRIC_WEAK) { quality = (int) getLong(PASSWORD_TYPE_ALTERNATE_KEY, - DevicePolicyManager.PASSWORD_QUALITY_SOMETHING); + DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED); } return quality; } @@ -730,7 +804,7 @@ public class LockPatternUtils { */ public boolean usingBiometricWeak() { int quality = - (int) getLong(PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_SOMETHING); + (int) getLong(PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED); return quality == DevicePolicyManager.PASSWORD_QUALITY_BIOMETRIC_WEAK; } @@ -869,11 +943,12 @@ public class LockPatternUtils { */ public boolean isLockPatternEnabled() { final boolean backupEnabled = - getLong(PASSWORD_TYPE_ALTERNATE_KEY, DevicePolicyManager.PASSWORD_QUALITY_SOMETHING) - == DevicePolicyManager.PASSWORD_QUALITY_SOMETHING; + getLong(PASSWORD_TYPE_ALTERNATE_KEY, + DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED) + == DevicePolicyManager.PASSWORD_QUALITY_SOMETHING; return getBoolean(Settings.Secure.LOCK_PATTERN_ENABLED, false) - && (getLong(PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_SOMETHING) + && (getLong(PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED) == DevicePolicyManager.PASSWORD_QUALITY_SOMETHING || (usingBiometricWeak() && backupEnabled)); } @@ -1201,7 +1276,7 @@ public class LockPatternUtils { private void setLong(String secureSettingKey, long value, int userHandle) { try { - getLockSettings().setLong(secureSettingKey, value, getCurrentOrCallingUserId()); + getLockSettings().setLong(secureSettingKey, value, userHandle); } catch (RemoteException re) { // What can we do? Log.e(TAG, "Couldn't write long " + secureSettingKey + re); @@ -1360,4 +1435,38 @@ public class LockPatternUtils { setBoolean(LOCKSCREEN_WIDGETS_ENABLED, enabled, userId); } + public void setEnabledTrustAgents(Collection<ComponentName> activeTrustAgents) { + setEnabledTrustAgents(activeTrustAgents, getCurrentOrCallingUserId()); + } + + public List<ComponentName> getEnabledTrustAgents() { + return getEnabledTrustAgents(getCurrentOrCallingUserId()); + } + + public void setEnabledTrustAgents(Collection<ComponentName> activeTrustAgents, int userId) { + StringBuilder sb = new StringBuilder(); + for (ComponentName cn : activeTrustAgents) { + if (sb.length() > 0) { + sb.append(','); + } + sb.append(cn.flattenToShortString()); + } + setString(ENABLED_TRUST_AGENTS, sb.toString(), userId); + getTrustManager().reportEnabledTrustAgentsChanged(getCurrentOrCallingUserId()); + } + + public List<ComponentName> getEnabledTrustAgents(int userId) { + String serialized = getString(ENABLED_TRUST_AGENTS, userId); + if (TextUtils.isEmpty(serialized)) { + return null; + } + String[] split = serialized.split(","); + ArrayList<ComponentName> activeTrustAgents = new ArrayList<ComponentName>(split.length); + for (String s : split) { + if (!TextUtils.isEmpty(s)) { + activeTrustAgents.add(ComponentName.unflattenFromString(s)); + } + } + return activeTrustAgents; + } } diff --git a/core/java/com/android/internal/widget/LockPatternView.java b/core/java/com/android/internal/widget/LockPatternView.java index b066d70..36ed344 100644 --- a/core/java/com/android/internal/widget/LockPatternView.java +++ b/core/java/com/android/internal/widget/LockPatternView.java @@ -32,6 +32,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; import android.util.AttributeSet; +import android.util.TypedValue; import android.view.HapticFeedbackConstants; import android.view.MotionEvent; import android.view.View; @@ -260,13 +261,23 @@ public class LockPatternView extends View { mPathPaint.setAntiAlias(true); mPathPaint.setDither(true); - mPathPaint.setColor(Color.WHITE); // TODO this should be from the style + + int defaultColor = Color.WHITE; + TypedValue outValue = new TypedValue(); + if (context.getTheme().resolveAttribute(android.R.attr.textColorPrimary, outValue, true)) { + defaultColor = context.getResources().getColor(outValue.resourceId); + } + + final int color = a.getColor(R.styleable.LockPatternView_pathColor, defaultColor); + mPathPaint.setColor(color); + mPathPaint.setAlpha(mStrokeAlpha); mPathPaint.setStyle(Paint.Style.STROKE); mPathPaint.setStrokeJoin(Paint.Join.ROUND); mPathPaint.setStrokeCap(Paint.Cap.ROUND); // lot's of bitmaps! + // TODO: those bitmaps are hardcoded to the Holo Theme which should not be the case! mBitmapBtnDefault = getBitmapFor(R.drawable.btn_code_lock_default_holo); mBitmapBtnTouched = getBitmapFor(R.drawable.btn_code_lock_touched_holo); mBitmapCircleDefault = getBitmapFor(R.drawable.indicator_code_lock_point_area_default_holo); @@ -868,12 +879,10 @@ public class LockPatternView extends View { // TODO: the path should be created and cached every time we hit-detect a cell // only the last segment of the path should be computed here - // draw the path of the pattern (unless the user is in progress, and - // we are in stealth mode) - final boolean drawPath = (!mInStealthMode || mPatternDisplayMode == DisplayMode.Wrong); + // draw the path of the pattern (unless we are in stealth mode) + final boolean drawPath = !mInStealthMode; - // draw the arrows associated with the path (unless the user is in progress, and - // we are in stealth mode) + // draw the arrows associated with the path (unless we are in stealth mode) boolean oldFlag = (mPaint.getFlags() & Paint.FILTER_BITMAP_FLAG) != 0; mPaint.setFilterBitmap(true); // draw with higher quality since we render with transforms if (drawPath) { @@ -974,7 +983,7 @@ public class LockPatternView extends View { Bitmap outerCircle; Bitmap innerCircle; - if (!partOfPattern || (mInStealthMode && mPatternDisplayMode != DisplayMode.Wrong)) { + if (!partOfPattern || mInStealthMode) { // unselected circle outerCircle = mBitmapCircleDefault; innerCircle = mBitmapBtnDefault; diff --git a/core/java/com/android/internal/widget/PasswordEntryKeyboard.java b/core/java/com/android/internal/widget/PasswordEntryKeyboard.java index 3c01c69..7483e75 100644 --- a/core/java/com/android/internal/widget/PasswordEntryKeyboard.java +++ b/core/java/com/android/internal/widget/PasswordEntryKeyboard.java @@ -16,7 +16,6 @@ package com.android.internal.widget; -import java.util.Locale; import android.content.Context; import android.content.res.Resources; import android.content.res.XmlResourceParser; @@ -73,8 +72,8 @@ public class PasswordEntryKeyboard extends Keyboard { private void init(Context context) { final Resources res = context.getResources(); - mShiftIcon = res.getDrawable(R.drawable.sym_keyboard_shift); - mShiftLockIcon = res.getDrawable(R.drawable.sym_keyboard_shift_locked); + mShiftIcon = context.getDrawable(R.drawable.sym_keyboard_shift); + mShiftLockIcon = context.getDrawable(R.drawable.sym_keyboard_shift_locked); sSpacebarVerticalCorrection = res.getDimensionPixelOffset( R.dimen.password_keyboard_spacebar_vertical_correction); } diff --git a/core/java/com/android/internal/widget/PasswordEntryKeyboardHelper.java b/core/java/com/android/internal/widget/PasswordEntryKeyboardHelper.java index a3df291..b2c9dc5 100644 --- a/core/java/com/android/internal/widget/PasswordEntryKeyboardHelper.java +++ b/core/java/com/android/internal/widget/PasswordEntryKeyboardHelper.java @@ -21,9 +21,7 @@ import android.content.res.Resources; import android.inputmethodservice.Keyboard; import android.inputmethodservice.KeyboardView; import android.inputmethodservice.KeyboardView.OnKeyboardActionListener; -import android.os.Handler; import android.os.SystemClock; -import android.os.Vibrator; import android.provider.Settings; import android.util.Log; import android.view.HapticFeedbackConstants; diff --git a/core/java/com/android/internal/widget/PasswordEntryKeyboardView.java b/core/java/com/android/internal/widget/PasswordEntryKeyboardView.java index b37adff..d27346b 100644 --- a/core/java/com/android/internal/widget/PasswordEntryKeyboardView.java +++ b/core/java/com/android/internal/widget/PasswordEntryKeyboardView.java @@ -29,11 +29,16 @@ public class PasswordEntryKeyboardView extends KeyboardView { static final int KEYCODE_NEXT_LANGUAGE = -104; public PasswordEntryKeyboardView(Context context, AttributeSet attrs) { - super(context, attrs); + this(context, attrs, 0); } - public PasswordEntryKeyboardView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public PasswordEntryKeyboardView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public PasswordEntryKeyboardView( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); } @Override diff --git a/core/java/com/android/internal/widget/PointerLocationView.java b/core/java/com/android/internal/widget/PointerLocationView.java index d82831f..e339c44 100644 --- a/core/java/com/android/internal/widget/PointerLocationView.java +++ b/core/java/com/android/internal/widget/PointerLocationView.java @@ -31,11 +31,13 @@ import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; +import android.view.WindowManagerPolicy.PointerEventListener; import android.view.MotionEvent.PointerCoords; import java.util.ArrayList; -public class PointerLocationView extends View implements InputDeviceListener { +public class PointerLocationView extends View implements InputDeviceListener, + PointerEventListener { private static final String TAG = "Pointer"; // The system property key used to specify an alternate velocity tracker strategy @@ -520,7 +522,8 @@ public class PointerLocationView extends View implements InputDeviceListener { .toString()); } - public void addPointerEvent(MotionEvent event) { + @Override + public void onPointerEvent(MotionEvent event) { final int action = event.getAction(); int NP = mPointers.size(); @@ -648,7 +651,7 @@ public class PointerLocationView extends View implements InputDeviceListener { @Override public boolean onTouchEvent(MotionEvent event) { - addPointerEvent(event); + onPointerEvent(event); if (event.getAction() == MotionEvent.ACTION_DOWN && !isFocused()) { requestFocus(); @@ -660,7 +663,7 @@ public class PointerLocationView extends View implements InputDeviceListener { public boolean onGenericMotionEvent(MotionEvent event) { final int source = event.getSource(); if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) { - addPointerEvent(event); + onPointerEvent(event); } else if ((source & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) { logMotionEvent("Joystick", event); } else if ((source & InputDevice.SOURCE_CLASS_POSITION) != 0) { diff --git a/core/java/com/android/internal/widget/RotarySelector.java b/core/java/com/android/internal/widget/RotarySelector.java index 4e405f4..64ce918 100644 --- a/core/java/com/android/internal/widget/RotarySelector.java +++ b/core/java/com/android/internal/widget/RotarySelector.java @@ -24,7 +24,7 @@ import android.graphics.Paint; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Matrix; -import android.graphics.drawable.Drawable; +import android.media.AudioManager; import android.os.UserHandle; import android.os.Vibrator; import android.provider.Settings; @@ -35,7 +35,9 @@ import android.view.View; import android.view.VelocityTracker; import android.view.ViewConfiguration; import android.view.animation.DecelerateInterpolator; + import static android.view.animation.AnimationUtils.currentAnimationTimeMillis; + import com.android.internal.R; @@ -677,7 +679,7 @@ public class RotarySelector extends View { mVibrator = (android.os.Vibrator) getContext() .getSystemService(Context.VIBRATOR_SERVICE); } - mVibrator.vibrate(duration); + mVibrator.vibrate(duration, AudioManager.STREAM_SYSTEM); } } diff --git a/core/java/com/android/internal/widget/ScrollingTabContainerView.java b/core/java/com/android/internal/widget/ScrollingTabContainerView.java index fa29e6e..d6bd1d6 100644 --- a/core/java/com/android/internal/widget/ScrollingTabContainerView.java +++ b/core/java/com/android/internal/widget/ScrollingTabContainerView.java @@ -23,7 +23,6 @@ import android.animation.TimeInterpolator; import android.app.ActionBar; import android.content.Context; import android.content.res.Configuration; -import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.text.TextUtils; import android.text.TextUtils.TruncateAt; diff --git a/core/java/com/android/internal/widget/SizeAdaptiveLayout.java b/core/java/com/android/internal/widget/SizeAdaptiveLayout.java index ba113a3..5f3c5f9 100644 --- a/core/java/com/android/internal/widget/SizeAdaptiveLayout.java +++ b/core/java/com/android/internal/widget/SizeAdaptiveLayout.java @@ -79,17 +79,20 @@ public class SizeAdaptiveLayout extends ViewGroup { private int mModestyPanelTop; public SizeAdaptiveLayout(Context context) { - super(context); - initialize(); + this(context, null); } public SizeAdaptiveLayout(Context context, AttributeSet attrs) { - super(context, attrs); - initialize(); + this(context, attrs, 0); + } + + public SizeAdaptiveLayout(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); } - public SizeAdaptiveLayout(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public SizeAdaptiveLayout( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); initialize(); } @@ -104,8 +107,6 @@ public class SizeAdaptiveLayout extends ViewGroup { } if (background instanceof ColorDrawable) { mModestyPanel.setBackgroundDrawable(background); - } else { - mModestyPanel.setBackgroundColor(Color.BLACK); } SizeAdaptiveLayout.LayoutParams layout = new SizeAdaptiveLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, @@ -151,6 +152,10 @@ public class SizeAdaptiveLayout extends ViewGroup { if (DEBUG) Log.d(TAG, this + " measure spec: " + MeasureSpec.toString(heightMeasureSpec)); View model = selectActiveChild(heightMeasureSpec); + if (model == null) { + setMeasuredDimension(0, 0); + return; + } SizeAdaptiveLayout.LayoutParams lp = (SizeAdaptiveLayout.LayoutParams) model.getLayoutParams(); if (DEBUG) Log.d(TAG, "active min: " + lp.minHeight + " max: " + lp.maxHeight); @@ -239,6 +244,8 @@ public class SizeAdaptiveLayout extends ViewGroup { int measureSpec = View.MeasureSpec.makeMeasureSpec(bottom - top, View.MeasureSpec.EXACTLY); mActiveChild = selectActiveChild(measureSpec); + if (mActiveChild == null) return; + mActiveChild.setVisibility(View.VISIBLE); if (mLastActive != mActiveChild && mLastActive != null) { diff --git a/core/java/com/android/internal/widget/SlidingTab.java b/core/java/com/android/internal/widget/SlidingTab.java index aebc4f6..deb0fd7 100644 --- a/core/java/com/android/internal/widget/SlidingTab.java +++ b/core/java/com/android/internal/widget/SlidingTab.java @@ -21,6 +21,7 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.media.AudioManager; import android.os.UserHandle; import android.os.Vibrator; import android.provider.Settings; @@ -821,7 +822,7 @@ public class SlidingTab extends ViewGroup { mVibrator = (android.os.Vibrator) getContext() .getSystemService(Context.VIBRATOR_SERVICE); } - mVibrator.vibrate(duration); + mVibrator.vibrate(duration, AudioManager.STREAM_SYSTEM); } } diff --git a/core/java/com/android/internal/widget/SubtitleView.java b/core/java/com/android/internal/widget/SubtitleView.java index 071193c..117463a 100644 --- a/core/java/com/android/internal/widget/SubtitleView.java +++ b/core/java/com/android/internal/widget/SubtitleView.java @@ -18,7 +18,6 @@ package com.android.internal.widget; import android.content.ContentResolver; import android.content.Context; -import android.content.res.Resources.Theme; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Canvas; @@ -33,7 +32,6 @@ import android.text.StaticLayout; import android.text.TextPaint; import android.util.AttributeSet; import android.util.DisplayMetrics; -import android.util.TypedValue; import android.view.View; import android.view.accessibility.CaptioningManager.CaptionStyle; @@ -41,6 +39,12 @@ public class SubtitleView extends View { // Ratio of inner padding to font size. private static final float INNER_PADDING_RATIO = 0.125f; + /** Color used for the shadowed edge of a bevel. */ + private static final int COLOR_BEVEL_DARK = 0x80000000; + + /** Color used for the illuminated edge of a bevel. */ + private static final int COLOR_BEVEL_LIGHT = 0x80FFFFFF; + // Styled dimensions. private final float mCornerRadius; private final float mOutlineWidth; @@ -79,12 +83,15 @@ public class SubtitleView extends View { this(context, attrs, 0); } - public SubtitleView(Context context, AttributeSet attrs, int defStyle) { + public SubtitleView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public SubtitleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs); - final Theme theme = context.getTheme(); - final TypedArray a = theme.obtainStyledAttributes( - attrs, android.R.styleable.TextView, defStyle, 0); + final TypedArray a = context.obtainStyledAttributes( + attrs, android.R.styleable.TextView, defStyleAttr, defStyleRes); CharSequence text = ""; int textSize = 15; @@ -112,7 +119,6 @@ public class SubtitleView extends View { // Set up density-dependent properties. // TODO: Move these to a default style. final Resources res = getContext().getResources(); - final DisplayMetrics m = res.getDisplayMetrics(); mCornerRadius = res.getDimensionPixelSize(com.android.internal.R.dimen.subtitle_corner_radius); mOutlineWidth = res.getDimensionPixelSize(com.android.internal.R.dimen.subtitle_outline_width); mShadowRadius = res.getDimensionPixelSize(com.android.internal.R.dimen.subtitle_shadow_radius); @@ -311,7 +317,8 @@ public class SubtitleView extends View { } } - if (mEdgeType == CaptionStyle.EDGE_TYPE_OUTLINE) { + final int edgeType = mEdgeType; + if (edgeType == CaptionStyle.EDGE_TYPE_OUTLINE) { textPaint.setStrokeJoin(Join.ROUND); textPaint.setStrokeWidth(mOutlineWidth); textPaint.setColor(mEdgeColor); @@ -320,8 +327,24 @@ public class SubtitleView extends View { for (int i = 0; i < lineCount; i++) { layout.drawText(c, i, i); } - } else if (mEdgeType == CaptionStyle.EDGE_TYPE_DROP_SHADOW) { + } else if (edgeType == CaptionStyle.EDGE_TYPE_DROP_SHADOW) { textPaint.setShadowLayer(mShadowRadius, mShadowOffsetX, mShadowOffsetY, mEdgeColor); + } else if (edgeType == CaptionStyle.EDGE_TYPE_RAISED + || edgeType == CaptionStyle.EDGE_TYPE_DEPRESSED) { + final boolean raised = edgeType == CaptionStyle.EDGE_TYPE_RAISED; + final int colorUp = raised ? Color.WHITE : mEdgeColor; + final int colorDown = raised ? mEdgeColor : Color.WHITE; + final float offset = mShadowRadius / 2f; + + textPaint.setColor(mForegroundColor); + textPaint.setStyle(Style.FILL); + textPaint.setShadowLayer(mShadowRadius, -offset, -offset, colorUp); + + for (int i = 0; i < lineCount; i++) { + layout.drawText(c, i, i); + } + + textPaint.setShadowLayer(mShadowRadius, offset, offset, colorDown); } textPaint.setColor(mForegroundColor); diff --git a/core/java/com/android/internal/widget/TextProgressBar.java b/core/java/com/android/internal/widget/TextProgressBar.java index e898aa4..7ca07d4 100644 --- a/core/java/com/android/internal/widget/TextProgressBar.java +++ b/core/java/com/android/internal/widget/TextProgressBar.java @@ -19,7 +19,6 @@ package com.android.internal.widget; import android.content.Context; import android.os.SystemClock; import android.util.AttributeSet; -import android.util.Log; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; @@ -59,9 +58,13 @@ public class TextProgressBar extends RelativeLayout implements OnChronometerTick boolean mChronometerFollow = false; int mChronometerGravity = Gravity.NO_GRAVITY; + + public TextProgressBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } - public TextProgressBar(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public TextProgressBar(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); } public TextProgressBar(Context context, AttributeSet attrs) { diff --git a/core/java/com/android/internal/widget/WaveView.java b/core/java/com/android/internal/widget/WaveView.java index d33d50c..86f14b3 100644 --- a/core/java/com/android/internal/widget/WaveView.java +++ b/core/java/com/android/internal/widget/WaveView.java @@ -25,10 +25,10 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.drawable.BitmapDrawable; +import android.media.AudioManager; import android.os.UserHandle; import android.os.Vibrator; import android.provider.Settings; -import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; @@ -583,7 +583,7 @@ public class WaveView extends View implements ValueAnimator.AnimatorUpdateListen mVibrator = (android.os.Vibrator) getContext() .getSystemService(Context.VIBRATOR_SERVICE); } - mVibrator.vibrate(duration); + mVibrator.vibrate(duration, AudioManager.STREAM_SYSTEM); } } diff --git a/core/java/com/android/internal/widget/multiwaveview/GlowPadView.java b/core/java/com/android/internal/widget/multiwaveview/GlowPadView.java index cd1ccd3..772dc5f 100644 --- a/core/java/com/android/internal/widget/multiwaveview/GlowPadView.java +++ b/core/java/com/android/internal/widget/multiwaveview/GlowPadView.java @@ -30,6 +30,7 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.drawable.Drawable; +import android.media.AudioManager; import android.os.Bundle; import android.os.UserHandle; import android.os.Vibrator; @@ -234,7 +235,7 @@ public class GlowPadView extends View { mMagneticTargets = a.getBoolean(R.styleable.GlowPadView_magneticTargets, mMagneticTargets); int pointId = getResourceId(a, R.styleable.GlowPadView_pointDrawable); - Drawable pointDrawable = pointId != 0 ? res.getDrawable(pointId) : null; + Drawable pointDrawable = pointId != 0 ? context.getDrawable(pointId) : null; mGlowRadius = a.getDimension(R.styleable.GlowPadView_glowRadius, 0.0f); TypedValue outValue = new TypedValue(); @@ -564,7 +565,7 @@ public class GlowPadView extends View { mContext.getContentResolver(), Settings.System.HAPTIC_FEEDBACK_ENABLED, 1, UserHandle.USER_CURRENT) != 0; if (mVibrator != null && hapticEnabled) { - mVibrator.vibrate(mVibrationDuration); + mVibrator.vibrate(mVibrationDuration, AudioManager.STREAM_SYSTEM); } } diff --git a/core/java/com/android/internal/widget/multiwaveview/MultiWaveView.java b/core/java/com/android/internal/widget/multiwaveview/MultiWaveView.java deleted file mode 100644 index e22d1e8..0000000 --- a/core/java/com/android/internal/widget/multiwaveview/MultiWaveView.java +++ /dev/null @@ -1,1269 +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 com.android.internal.widget.multiwaveview; - -import android.animation.Animator; -import android.animation.Animator.AnimatorListener; -import android.animation.AnimatorListenerAdapter; -import android.animation.TimeInterpolator; -import android.animation.ValueAnimator; -import android.animation.ValueAnimator.AnimatorUpdateListener; -import android.content.ComponentName; -import android.content.Context; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.RectF; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.os.UserHandle; -import android.os.Vibrator; -import android.provider.Settings; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.util.Log; -import android.util.TypedValue; -import android.view.Gravity; -import android.view.MotionEvent; -import android.view.View; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityManager; - -import com.android.internal.R; - -import java.util.ArrayList; - -/** - * A special widget containing a center and outer ring. Moving the center ring to the outer ring - * causes an event that can be caught by implementing OnTriggerListener. - */ -public class MultiWaveView extends View { - private static final String TAG = "MultiWaveView"; - private static final boolean DEBUG = false; - - // Wave state machine - private static final int STATE_IDLE = 0; - private static final int STATE_START = 1; - private static final int STATE_FIRST_TOUCH = 2; - private static final int STATE_TRACKING = 3; - private static final int STATE_SNAP = 4; - private static final int STATE_FINISH = 5; - - // Animation properties. - private static final float SNAP_MARGIN_DEFAULT = 20.0f; // distance to ring before we snap to it - - public interface OnTriggerListener { - int NO_HANDLE = 0; - int CENTER_HANDLE = 1; - public void onGrabbed(View v, int handle); - public void onReleased(View v, int handle); - public void onTrigger(View v, int target); - public void onGrabbedStateChange(View v, int handle); - public void onFinishFinalAnimation(); - } - - // Tuneable parameters for animation - private static final int CHEVRON_INCREMENTAL_DELAY = 160; - private static final int CHEVRON_ANIMATION_DURATION = 850; - private static final int RETURN_TO_HOME_DELAY = 1200; - private static final int RETURN_TO_HOME_DURATION = 200; - private static final int HIDE_ANIMATION_DELAY = 200; - private static final int HIDE_ANIMATION_DURATION = 200; - private static final int SHOW_ANIMATION_DURATION = 200; - private static final int SHOW_ANIMATION_DELAY = 50; - private static final int INITIAL_SHOW_HANDLE_DURATION = 200; - - private static final float TAP_RADIUS_SCALE_ACCESSIBILITY_ENABLED = 1.3f; - private static final float TARGET_SCALE_EXPANDED = 1.0f; - private static final float TARGET_SCALE_COLLAPSED = 0.8f; - private static final float RING_SCALE_EXPANDED = 1.0f; - private static final float RING_SCALE_COLLAPSED = 0.5f; - - private TimeInterpolator mChevronAnimationInterpolator = Ease.Quad.easeOut; - - private ArrayList<TargetDrawable> mTargetDrawables = new ArrayList<TargetDrawable>(); - private ArrayList<TargetDrawable> mChevronDrawables = new ArrayList<TargetDrawable>(); - private AnimationBundle mChevronAnimations = new AnimationBundle(); - private AnimationBundle mTargetAnimations = new AnimationBundle(); - private AnimationBundle mHandleAnimations = new AnimationBundle(); - private ArrayList<String> mTargetDescriptions; - private ArrayList<String> mDirectionDescriptions; - private OnTriggerListener mOnTriggerListener; - private TargetDrawable mHandleDrawable; - private TargetDrawable mOuterRing; - private Vibrator mVibrator; - - private int mFeedbackCount = 3; - private int mVibrationDuration = 0; - private int mGrabbedState; - private int mActiveTarget = -1; - private float mTapRadius; - private float mWaveCenterX; - private float mWaveCenterY; - private int mMaxTargetHeight; - private int mMaxTargetWidth; - - private float mOuterRadius = 0.0f; - private float mSnapMargin = 0.0f; - private boolean mDragging; - private int mNewTargetResources; - - private class AnimationBundle extends ArrayList<Tweener> { - private static final long serialVersionUID = 0xA84D78726F127468L; - private boolean mSuspended; - - public void start() { - if (mSuspended) return; // ignore attempts to start animations - final int count = size(); - for (int i = 0; i < count; i++) { - Tweener anim = get(i); - anim.animator.start(); - } - } - - public void cancel() { - final int count = size(); - for (int i = 0; i < count; i++) { - Tweener anim = get(i); - anim.animator.cancel(); - } - clear(); - } - - public void stop() { - final int count = size(); - for (int i = 0; i < count; i++) { - Tweener anim = get(i); - anim.animator.end(); - } - clear(); - } - - public void setSuspended(boolean suspend) { - mSuspended = suspend; - } - }; - - private AnimatorListener mResetListener = new AnimatorListenerAdapter() { - public void onAnimationEnd(Animator animator) { - switchToState(STATE_IDLE, mWaveCenterX, mWaveCenterY); - dispatchOnFinishFinalAnimation(); - } - }; - - private AnimatorListener mResetListenerWithPing = new AnimatorListenerAdapter() { - public void onAnimationEnd(Animator animator) { - ping(); - switchToState(STATE_IDLE, mWaveCenterX, mWaveCenterY); - dispatchOnFinishFinalAnimation(); - } - }; - - private AnimatorUpdateListener mUpdateListener = new AnimatorUpdateListener() { - public void onAnimationUpdate(ValueAnimator animation) { - invalidateGlobalRegion(mHandleDrawable); - invalidate(); - } - }; - - private boolean mAnimatingTargets; - private AnimatorListener mTargetUpdateListener = new AnimatorListenerAdapter() { - public void onAnimationEnd(Animator animator) { - if (mNewTargetResources != 0) { - internalSetTargetResources(mNewTargetResources); - mNewTargetResources = 0; - hideTargets(false, false); - } - mAnimatingTargets = false; - } - }; - private int mTargetResourceId; - private int mTargetDescriptionsResourceId; - private int mDirectionDescriptionsResourceId; - private boolean mAlwaysTrackFinger; - private int mHorizontalInset; - private int mVerticalInset; - private int mGravity = Gravity.TOP; - private boolean mInitialLayout = true; - private Tweener mBackgroundAnimator; - - public MultiWaveView(Context context) { - this(context, null); - } - - public MultiWaveView(Context context, AttributeSet attrs) { - super(context, attrs); - Resources res = context.getResources(); - - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MultiWaveView); - mOuterRadius = a.getDimension(R.styleable.MultiWaveView_outerRadius, mOuterRadius); - mSnapMargin = a.getDimension(R.styleable.MultiWaveView_snapMargin, mSnapMargin); - mVibrationDuration = a.getInt(R.styleable.MultiWaveView_vibrationDuration, - mVibrationDuration); - mFeedbackCount = a.getInt(R.styleable.MultiWaveView_feedbackCount, - mFeedbackCount); - mHandleDrawable = new TargetDrawable(res, - a.peekValue(R.styleable.MultiWaveView_handleDrawable).resourceId); - mTapRadius = mHandleDrawable.getWidth()/2; - mOuterRing = new TargetDrawable(res, - a.peekValue(R.styleable.MultiWaveView_waveDrawable).resourceId); - mAlwaysTrackFinger = a.getBoolean(R.styleable.MultiWaveView_alwaysTrackFinger, false); - - // Read array of chevron drawables - TypedValue outValue = new TypedValue(); - if (a.getValue(R.styleable.MultiWaveView_chevronDrawables, outValue)) { - ArrayList<TargetDrawable> chevrons = loadDrawableArray(outValue.resourceId); - for (int i = 0; i < chevrons.size(); i++) { - final TargetDrawable chevron = chevrons.get(i); - for (int k = 0; k < mFeedbackCount; k++) { - mChevronDrawables.add(chevron == null ? null : new TargetDrawable(chevron)); - } - } - } - - // Read array of target drawables - if (a.getValue(R.styleable.MultiWaveView_targetDrawables, outValue)) { - internalSetTargetResources(outValue.resourceId); - } - if (mTargetDrawables == null || mTargetDrawables.size() == 0) { - throw new IllegalStateException("Must specify at least one target drawable"); - } - - // Read array of target descriptions - if (a.getValue(R.styleable.MultiWaveView_targetDescriptions, outValue)) { - final int resourceId = outValue.resourceId; - if (resourceId == 0) { - throw new IllegalStateException("Must specify target descriptions"); - } - setTargetDescriptionsResourceId(resourceId); - } - - // Read array of direction descriptions - if (a.getValue(R.styleable.MultiWaveView_directionDescriptions, outValue)) { - final int resourceId = outValue.resourceId; - if (resourceId == 0) { - throw new IllegalStateException("Must specify direction descriptions"); - } - setDirectionDescriptionsResourceId(resourceId); - } - - a.recycle(); - - // Use gravity attribute from LinearLayout - a = context.obtainStyledAttributes(attrs, android.R.styleable.LinearLayout); - mGravity = a.getInt(android.R.styleable.LinearLayout_gravity, Gravity.TOP); - a.recycle(); - - setVibrateEnabled(mVibrationDuration > 0); - assignDefaultsIfNeeded(); - } - - private void dump() { - Log.v(TAG, "Outer Radius = " + mOuterRadius); - Log.v(TAG, "SnapMargin = " + mSnapMargin); - Log.v(TAG, "FeedbackCount = " + mFeedbackCount); - Log.v(TAG, "VibrationDuration = " + mVibrationDuration); - Log.v(TAG, "TapRadius = " + mTapRadius); - Log.v(TAG, "WaveCenterX = " + mWaveCenterX); - Log.v(TAG, "WaveCenterY = " + mWaveCenterY); - } - - public void suspendAnimations() { - mChevronAnimations.setSuspended(true); - mTargetAnimations.setSuspended(true); - mHandleAnimations.setSuspended(true); - } - - public void resumeAnimations() { - mChevronAnimations.setSuspended(false); - mTargetAnimations.setSuspended(false); - mHandleAnimations.setSuspended(false); - mChevronAnimations.start(); - mTargetAnimations.start(); - mHandleAnimations.start(); - } - - @Override - protected int getSuggestedMinimumWidth() { - // View should be large enough to contain the background + handle and - // target drawable on either edge. - return (int) (Math.max(mOuterRing.getWidth(), 2 * mOuterRadius) + mMaxTargetWidth); - } - - @Override - protected int getSuggestedMinimumHeight() { - // View should be large enough to contain the unlock ring + target and - // target drawable on either edge - return (int) (Math.max(mOuterRing.getHeight(), 2 * mOuterRadius) + mMaxTargetHeight); - } - - private int resolveMeasured(int measureSpec, int desired) - { - int result = 0; - int specSize = MeasureSpec.getSize(measureSpec); - switch (MeasureSpec.getMode(measureSpec)) { - case MeasureSpec.UNSPECIFIED: - result = desired; - break; - case MeasureSpec.AT_MOST: - result = Math.min(specSize, desired); - break; - case MeasureSpec.EXACTLY: - default: - result = specSize; - } - return result; - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - final int minimumWidth = getSuggestedMinimumWidth(); - final int minimumHeight = getSuggestedMinimumHeight(); - int computedWidth = resolveMeasured(widthMeasureSpec, minimumWidth); - int computedHeight = resolveMeasured(heightMeasureSpec, minimumHeight); - computeInsets((computedWidth - minimumWidth), (computedHeight - minimumHeight)); - setMeasuredDimension(computedWidth, computedHeight); - } - - private void switchToState(int state, float x, float y) { - switch (state) { - case STATE_IDLE: - deactivateTargets(); - hideTargets(true, false); - startBackgroundAnimation(0, 0.0f); - mHandleDrawable.setState(TargetDrawable.STATE_INACTIVE); - break; - - case STATE_START: - deactivateHandle(0, 0, 1.0f, null); - startBackgroundAnimation(0, 0.0f); - break; - - case STATE_FIRST_TOUCH: - deactivateTargets(); - showTargets(true); - mHandleDrawable.setState(TargetDrawable.STATE_ACTIVE); - startBackgroundAnimation(INITIAL_SHOW_HANDLE_DURATION, 1.0f); - setGrabbedState(OnTriggerListener.CENTER_HANDLE); - if (AccessibilityManager.getInstance(mContext).isEnabled()) { - announceTargets(); - } - break; - - case STATE_TRACKING: - break; - - case STATE_SNAP: - break; - - case STATE_FINISH: - doFinish(); - break; - } - } - - private void activateHandle(int duration, int delay, float finalAlpha, - AnimatorListener finishListener) { - mHandleAnimations.cancel(); - mHandleAnimations.add(Tweener.to(mHandleDrawable, duration, - "ease", Ease.Cubic.easeIn, - "delay", delay, - "alpha", finalAlpha, - "onUpdate", mUpdateListener, - "onComplete", finishListener)); - mHandleAnimations.start(); - } - - private void deactivateHandle(int duration, int delay, float finalAlpha, - AnimatorListener finishListener) { - mHandleAnimations.cancel(); - mHandleAnimations.add(Tweener.to(mHandleDrawable, duration, - "ease", Ease.Quart.easeOut, - "delay", delay, - "alpha", finalAlpha, - "x", 0, - "y", 0, - "onUpdate", mUpdateListener, - "onComplete", finishListener)); - mHandleAnimations.start(); - } - - /** - * Animation used to attract user's attention to the target button. - * Assumes mChevronDrawables is an a list with an even number of chevrons filled with - * mFeedbackCount items in the order: left, right, top, bottom. - */ - private void startChevronAnimation() { - final float chevronStartDistance = mHandleDrawable.getWidth() * 0.8f; - final float chevronStopDistance = mOuterRadius * 0.9f / 2.0f; - final float startScale = 0.5f; - final float endScale = 2.0f; - final int directionCount = mFeedbackCount > 0 ? mChevronDrawables.size()/mFeedbackCount : 0; - - mChevronAnimations.stop(); - - // Add an animation for all chevron drawables. There are mFeedbackCount drawables - // in each direction and directionCount directions. - for (int direction = 0; direction < directionCount; direction++) { - double angle = 2.0 * Math.PI * direction / directionCount; - final float sx = (float) Math.cos(angle); - final float sy = 0.0f - (float) Math.sin(angle); - final float[] xrange = new float[] - {sx * chevronStartDistance, sx * chevronStopDistance}; - final float[] yrange = new float[] - {sy * chevronStartDistance, sy * chevronStopDistance}; - for (int count = 0; count < mFeedbackCount; count++) { - int delay = count * CHEVRON_INCREMENTAL_DELAY; - final TargetDrawable icon = mChevronDrawables.get(direction*mFeedbackCount + count); - if (icon == null) { - continue; - } - mChevronAnimations.add(Tweener.to(icon, CHEVRON_ANIMATION_DURATION, - "ease", mChevronAnimationInterpolator, - "delay", delay, - "x", xrange, - "y", yrange, - "alpha", new float[] {1.0f, 0.0f}, - "scaleX", new float[] {startScale, endScale}, - "scaleY", new float[] {startScale, endScale}, - "onUpdate", mUpdateListener)); - } - } - mChevronAnimations.start(); - } - - private void deactivateTargets() { - final int count = mTargetDrawables.size(); - for (int i = 0; i < count; i++) { - TargetDrawable target = mTargetDrawables.get(i); - target.setState(TargetDrawable.STATE_INACTIVE); - } - mActiveTarget = -1; - } - - void invalidateGlobalRegion(TargetDrawable drawable) { - int width = drawable.getWidth(); - int height = drawable.getHeight(); - RectF childBounds = new RectF(0, 0, width, height); - childBounds.offset(drawable.getX() - width/2, drawable.getY() - height/2); - View view = this; - while (view.getParent() != null && view.getParent() instanceof View) { - view = (View) view.getParent(); - view.getMatrix().mapRect(childBounds); - view.invalidate((int) Math.floor(childBounds.left), - (int) Math.floor(childBounds.top), - (int) Math.ceil(childBounds.right), - (int) Math.ceil(childBounds.bottom)); - } - } - - /** - * Dispatches a trigger event to listener. Ignored if a listener is not set. - * @param whichTarget the target that was triggered. - */ - private void dispatchTriggerEvent(int whichTarget) { - vibrate(); - if (mOnTriggerListener != null) { - mOnTriggerListener.onTrigger(this, whichTarget); - } - } - - private void dispatchOnFinishFinalAnimation() { - if (mOnTriggerListener != null) { - mOnTriggerListener.onFinishFinalAnimation(); - } - } - - private void doFinish() { - final int activeTarget = mActiveTarget; - final boolean targetHit = activeTarget != -1; - - if (targetHit) { - if (DEBUG) Log.v(TAG, "Finish with target hit = " + targetHit); - - highlightSelected(activeTarget); - - // Inform listener of any active targets. Typically only one will be active. - deactivateHandle(RETURN_TO_HOME_DURATION, RETURN_TO_HOME_DELAY, 0.0f, mResetListener); - dispatchTriggerEvent(activeTarget); - if (!mAlwaysTrackFinger) { - // Force ring and targets to finish animation to final expanded state - mTargetAnimations.stop(); - } - } else { - // Animate handle back to the center based on current state. - deactivateHandle(HIDE_ANIMATION_DURATION, HIDE_ANIMATION_DELAY, 1.0f, - mResetListenerWithPing); - hideTargets(true, false); - } - - setGrabbedState(OnTriggerListener.NO_HANDLE); - } - - private void highlightSelected(int activeTarget) { - // Highlight the given target and fade others - mTargetDrawables.get(activeTarget).setState(TargetDrawable.STATE_ACTIVE); - hideUnselected(activeTarget); - } - - private void hideUnselected(int active) { - for (int i = 0; i < mTargetDrawables.size(); i++) { - if (i != active) { - mTargetDrawables.get(i).setAlpha(0.0f); - } - } - } - - private void hideTargets(boolean animate, boolean expanded) { - mTargetAnimations.cancel(); - // Note: these animations should complete at the same time so that we can swap out - // the target assets asynchronously from the setTargetResources() call. - mAnimatingTargets = animate; - final int duration = animate ? HIDE_ANIMATION_DURATION : 0; - final int delay = animate ? HIDE_ANIMATION_DELAY : 0; - - final float targetScale = expanded ? TARGET_SCALE_EXPANDED : TARGET_SCALE_COLLAPSED; - final int length = mTargetDrawables.size(); - for (int i = 0; i < length; i++) { - TargetDrawable target = mTargetDrawables.get(i); - target.setState(TargetDrawable.STATE_INACTIVE); - mTargetAnimations.add(Tweener.to(target, duration, - "ease", Ease.Cubic.easeOut, - "alpha", 0.0f, - "scaleX", targetScale, - "scaleY", targetScale, - "delay", delay, - "onUpdate", mUpdateListener)); - } - - final float ringScaleTarget = expanded ? RING_SCALE_EXPANDED : RING_SCALE_COLLAPSED; - mTargetAnimations.add(Tweener.to(mOuterRing, duration, - "ease", Ease.Cubic.easeOut, - "alpha", 0.0f, - "scaleX", ringScaleTarget, - "scaleY", ringScaleTarget, - "delay", delay, - "onUpdate", mUpdateListener, - "onComplete", mTargetUpdateListener)); - - mTargetAnimations.start(); - } - - private void showTargets(boolean animate) { - mTargetAnimations.stop(); - mAnimatingTargets = animate; - final int delay = animate ? SHOW_ANIMATION_DELAY : 0; - final int duration = animate ? SHOW_ANIMATION_DURATION : 0; - final int length = mTargetDrawables.size(); - for (int i = 0; i < length; i++) { - TargetDrawable target = mTargetDrawables.get(i); - target.setState(TargetDrawable.STATE_INACTIVE); - mTargetAnimations.add(Tweener.to(target, duration, - "ease", Ease.Cubic.easeOut, - "alpha", 1.0f, - "scaleX", 1.0f, - "scaleY", 1.0f, - "delay", delay, - "onUpdate", mUpdateListener)); - } - mTargetAnimations.add(Tweener.to(mOuterRing, duration, - "ease", Ease.Cubic.easeOut, - "alpha", 1.0f, - "scaleX", 1.0f, - "scaleY", 1.0f, - "delay", delay, - "onUpdate", mUpdateListener, - "onComplete", mTargetUpdateListener)); - - mTargetAnimations.start(); - } - - private void vibrate() { - final boolean hapticEnabled = Settings.System.getIntForUser( - mContext.getContentResolver(), Settings.System.HAPTIC_FEEDBACK_ENABLED, 1, - UserHandle.USER_CURRENT) != 0; - if (mVibrator != null && hapticEnabled) { - mVibrator.vibrate(mVibrationDuration); - } - } - - private ArrayList<TargetDrawable> loadDrawableArray(int resourceId) { - Resources res = getContext().getResources(); - TypedArray array = res.obtainTypedArray(resourceId); - final int count = array.length(); - ArrayList<TargetDrawable> drawables = new ArrayList<TargetDrawable>(count); - for (int i = 0; i < count; i++) { - TypedValue value = array.peekValue(i); - TargetDrawable target = new TargetDrawable(res, value != null ? value.resourceId : 0); - drawables.add(target); - } - array.recycle(); - return drawables; - } - - private void internalSetTargetResources(int resourceId) { - mTargetDrawables = loadDrawableArray(resourceId); - mTargetResourceId = resourceId; - final int count = mTargetDrawables.size(); - int maxWidth = mHandleDrawable.getWidth(); - int maxHeight = mHandleDrawable.getHeight(); - for (int i = 0; i < count; i++) { - TargetDrawable target = mTargetDrawables.get(i); - maxWidth = Math.max(maxWidth, target.getWidth()); - maxHeight = Math.max(maxHeight, target.getHeight()); - } - if (mMaxTargetWidth != maxWidth || mMaxTargetHeight != maxHeight) { - mMaxTargetWidth = maxWidth; - mMaxTargetHeight = maxHeight; - requestLayout(); // required to resize layout and call updateTargetPositions() - } else { - updateTargetPositions(mWaveCenterX, mWaveCenterY); - updateChevronPositions(mWaveCenterX, mWaveCenterY); - } - } - - /** - * Loads an array of drawables from the given resourceId. - * - * @param resourceId - */ - public void setTargetResources(int resourceId) { - if (mAnimatingTargets) { - // postpone this change until we return to the initial state - mNewTargetResources = resourceId; - } else { - internalSetTargetResources(resourceId); - } - } - - public int getTargetResourceId() { - return mTargetResourceId; - } - - /** - * Sets the resource id specifying the target descriptions for accessibility. - * - * @param resourceId The resource id. - */ - public void setTargetDescriptionsResourceId(int resourceId) { - mTargetDescriptionsResourceId = resourceId; - if (mTargetDescriptions != null) { - mTargetDescriptions.clear(); - } - } - - /** - * Gets the resource id specifying the target descriptions for accessibility. - * - * @return The resource id. - */ - public int getTargetDescriptionsResourceId() { - return mTargetDescriptionsResourceId; - } - - /** - * Sets the resource id specifying the target direction descriptions for accessibility. - * - * @param resourceId The resource id. - */ - public void setDirectionDescriptionsResourceId(int resourceId) { - mDirectionDescriptionsResourceId = resourceId; - if (mDirectionDescriptions != null) { - mDirectionDescriptions.clear(); - } - } - - /** - * Gets the resource id specifying the target direction descriptions. - * - * @return The resource id. - */ - public int getDirectionDescriptionsResourceId() { - return mDirectionDescriptionsResourceId; - } - - /** - * Enable or disable vibrate on touch. - * - * @param enabled - */ - public void setVibrateEnabled(boolean enabled) { - if (enabled && mVibrator == null) { - mVibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE); - } else { - mVibrator = null; - } - } - - /** - * Starts chevron animation. Example use case: show chevron animation whenever the phone rings - * or the user touches the screen. - * - */ - public void ping() { - startChevronAnimation(); - } - - /** - * Resets the widget to default state and cancels all animation. If animate is 'true', will - * animate objects into place. Otherwise, objects will snap back to place. - * - * @param animate - */ - public void reset(boolean animate) { - mChevronAnimations.stop(); - mHandleAnimations.stop(); - mTargetAnimations.stop(); - startBackgroundAnimation(0, 0.0f); - hideChevrons(); - hideTargets(animate, false); - deactivateHandle(0, 0, 1.0f, null); - Tweener.reset(); - } - - private void startBackgroundAnimation(int duration, float alpha) { - Drawable background = getBackground(); - if (mAlwaysTrackFinger && background != null) { - if (mBackgroundAnimator != null) { - mBackgroundAnimator.animator.end(); - } - mBackgroundAnimator = Tweener.to(background, duration, - "ease", Ease.Cubic.easeIn, - "alpha", new int[] {0, (int)(255.0f * alpha)}, - "delay", SHOW_ANIMATION_DELAY); - mBackgroundAnimator.animator.start(); - } - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - final int action = event.getAction(); - boolean handled = false; - switch (action) { - case MotionEvent.ACTION_DOWN: - if (DEBUG) Log.v(TAG, "*** DOWN ***"); - handleDown(event); - handled = true; - break; - - case MotionEvent.ACTION_MOVE: - if (DEBUG) Log.v(TAG, "*** MOVE ***"); - handleMove(event); - handled = true; - break; - - case MotionEvent.ACTION_UP: - if (DEBUG) Log.v(TAG, "*** UP ***"); - handleMove(event); - handleUp(event); - handled = true; - break; - - case MotionEvent.ACTION_CANCEL: - if (DEBUG) Log.v(TAG, "*** CANCEL ***"); - handleMove(event); - handleCancel(event); - handled = true; - break; - } - invalidate(); - return handled ? true : super.onTouchEvent(event); - } - - private void moveHandleTo(float x, float y, boolean animate) { - mHandleDrawable.setX(x); - mHandleDrawable.setY(y); - } - - private void handleDown(MotionEvent event) { - float eventX = event.getX(); - float eventY = event.getY(); - switchToState(STATE_START, eventX, eventY); - if (!trySwitchToFirstTouchState(eventX, eventY)) { - mDragging = false; - ping(); - } - } - - private void handleUp(MotionEvent event) { - if (DEBUG && mDragging) Log.v(TAG, "** Handle RELEASE"); - switchToState(STATE_FINISH, event.getX(), event.getY()); - } - - private void handleCancel(MotionEvent event) { - if (DEBUG && mDragging) Log.v(TAG, "** Handle CANCEL"); - - // We should drop the active target here but it interferes with - // moving off the screen in the direction of the navigation bar. At some point we may - // want to revisit how we handle this. For now we'll allow a canceled event to - // activate the current target. - - // mActiveTarget = -1; // Drop the active target if canceled. - - switchToState(STATE_FINISH, event.getX(), event.getY()); - } - - private void handleMove(MotionEvent event) { - int activeTarget = -1; - final int historySize = event.getHistorySize(); - ArrayList<TargetDrawable> targets = mTargetDrawables; - int ntargets = targets.size(); - float x = 0.0f; - float y = 0.0f; - for (int k = 0; k < historySize + 1; k++) { - float eventX = k < historySize ? event.getHistoricalX(k) : event.getX(); - float eventY = k < historySize ? event.getHistoricalY(k) : event.getY(); - // tx and ty are relative to wave center - float tx = eventX - mWaveCenterX; - float ty = eventY - mWaveCenterY; - float touchRadius = (float) Math.sqrt(dist2(tx, ty)); - final float scale = touchRadius > mOuterRadius ? mOuterRadius / touchRadius : 1.0f; - float limitX = tx * scale; - float limitY = ty * scale; - double angleRad = Math.atan2(-ty, tx); - - if (!mDragging) { - trySwitchToFirstTouchState(eventX, eventY); - } - - if (mDragging) { - // For multiple targets, snap to the one that matches - final float snapRadius = mOuterRadius - mSnapMargin; - final float snapDistance2 = snapRadius * snapRadius; - // Find first target in range - for (int i = 0; i < ntargets; i++) { - TargetDrawable target = targets.get(i); - - double targetMinRad = (i - 0.5) * 2 * Math.PI / ntargets; - double targetMaxRad = (i + 0.5) * 2 * Math.PI / ntargets; - if (target.isEnabled()) { - boolean angleMatches = - (angleRad > targetMinRad && angleRad <= targetMaxRad) || - (angleRad + 2 * Math.PI > targetMinRad && - angleRad + 2 * Math.PI <= targetMaxRad); - if (angleMatches && (dist2(tx, ty) > snapDistance2)) { - activeTarget = i; - } - } - } - } - x = limitX; - y = limitY; - } - - if (!mDragging) { - return; - } - - if (activeTarget != -1) { - switchToState(STATE_SNAP, x,y); - moveHandleTo(x, y, false); - } else { - switchToState(STATE_TRACKING, x, y); - moveHandleTo(x, y, false); - } - - // Draw handle outside parent's bounds - invalidateGlobalRegion(mHandleDrawable); - - if (mActiveTarget != activeTarget) { - // Defocus the old target - if (mActiveTarget != -1) { - TargetDrawable target = targets.get(mActiveTarget); - if (target.hasState(TargetDrawable.STATE_FOCUSED)) { - target.setState(TargetDrawable.STATE_INACTIVE); - } - } - // Focus the new target - if (activeTarget != -1) { - TargetDrawable target = targets.get(activeTarget); - if (target.hasState(TargetDrawable.STATE_FOCUSED)) { - target.setState(TargetDrawable.STATE_FOCUSED); - } - if (AccessibilityManager.getInstance(mContext).isEnabled()) { - String targetContentDescription = getTargetDescription(activeTarget); - announceText(targetContentDescription); - } - activateHandle(0, 0, 0.0f, null); - } else { - activateHandle(0, 0, 1.0f, null); - } - } - mActiveTarget = activeTarget; - } - - @Override - public boolean onHoverEvent(MotionEvent event) { - if (AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled()) { - final int action = event.getAction(); - switch (action) { - case MotionEvent.ACTION_HOVER_ENTER: - event.setAction(MotionEvent.ACTION_DOWN); - break; - case MotionEvent.ACTION_HOVER_MOVE: - event.setAction(MotionEvent.ACTION_MOVE); - break; - case MotionEvent.ACTION_HOVER_EXIT: - event.setAction(MotionEvent.ACTION_UP); - break; - } - onTouchEvent(event); - event.setAction(action); - } - return super.onHoverEvent(event); - } - - /** - * Sets the current grabbed state, and dispatches a grabbed state change - * event to our listener. - */ - private void setGrabbedState(int newState) { - if (newState != mGrabbedState) { - if (newState != OnTriggerListener.NO_HANDLE) { - vibrate(); - } - mGrabbedState = newState; - if (mOnTriggerListener != null) { - if (newState == OnTriggerListener.NO_HANDLE) { - mOnTriggerListener.onReleased(this, OnTriggerListener.CENTER_HANDLE); - } else { - mOnTriggerListener.onGrabbed(this, OnTriggerListener.CENTER_HANDLE); - } - mOnTriggerListener.onGrabbedStateChange(this, newState); - } - } - } - - private boolean trySwitchToFirstTouchState(float x, float y) { - final float tx = x - mWaveCenterX; - final float ty = y - mWaveCenterY; - if (mAlwaysTrackFinger || dist2(tx,ty) <= getScaledTapRadiusSquared()) { - if (DEBUG) Log.v(TAG, "** Handle HIT"); - switchToState(STATE_FIRST_TOUCH, x, y); - moveHandleTo(tx, ty, false); - mDragging = true; - return true; - } - return false; - } - - private void assignDefaultsIfNeeded() { - if (mOuterRadius == 0.0f) { - mOuterRadius = Math.max(mOuterRing.getWidth(), mOuterRing.getHeight())/2.0f; - } - if (mSnapMargin == 0.0f) { - mSnapMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, - SNAP_MARGIN_DEFAULT, getContext().getResources().getDisplayMetrics()); - } - } - - private void computeInsets(int dx, int dy) { - final int layoutDirection = getLayoutDirection(); - final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection); - - switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { - case Gravity.LEFT: - mHorizontalInset = 0; - break; - case Gravity.RIGHT: - mHorizontalInset = dx; - break; - case Gravity.CENTER_HORIZONTAL: - default: - mHorizontalInset = dx / 2; - break; - } - switch (absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK) { - case Gravity.TOP: - mVerticalInset = 0; - break; - case Gravity.BOTTOM: - mVerticalInset = dy; - break; - case Gravity.CENTER_VERTICAL: - default: - mVerticalInset = dy / 2; - break; - } - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - final int width = right - left; - final int height = bottom - top; - - // Target placement width/height. This puts the targets on the greater of the ring - // width or the specified outer radius. - final float placementWidth = Math.max(mOuterRing.getWidth(), 2 * mOuterRadius); - final float placementHeight = Math.max(mOuterRing.getHeight(), 2 * mOuterRadius); - float newWaveCenterX = mHorizontalInset - + Math.max(width, mMaxTargetWidth + placementWidth) / 2; - float newWaveCenterY = mVerticalInset - + Math.max(height, + mMaxTargetHeight + placementHeight) / 2; - - if (mInitialLayout) { - hideChevrons(); - hideTargets(false, false); - moveHandleTo(0, 0, false); - mInitialLayout = false; - } - - mOuterRing.setPositionX(newWaveCenterX); - mOuterRing.setPositionY(newWaveCenterY); - - mHandleDrawable.setPositionX(newWaveCenterX); - mHandleDrawable.setPositionY(newWaveCenterY); - - updateTargetPositions(newWaveCenterX, newWaveCenterY); - updateChevronPositions(newWaveCenterX, newWaveCenterY); - - mWaveCenterX = newWaveCenterX; - mWaveCenterY = newWaveCenterY; - - if (DEBUG) dump(); - } - - private void updateTargetPositions(float centerX, float centerY) { - // Reposition the target drawables if the view changed. - ArrayList<TargetDrawable> targets = mTargetDrawables; - final int size = targets.size(); - final float alpha = (float) (-2.0f * Math.PI / size); - for (int i = 0; i < size; i++) { - final TargetDrawable targetIcon = targets.get(i); - final float angle = alpha * i; - targetIcon.setPositionX(centerX); - targetIcon.setPositionY(centerY); - targetIcon.setX(mOuterRadius * (float) Math.cos(angle)); - targetIcon.setY(mOuterRadius * (float) Math.sin(angle)); - } - } - - private void updateChevronPositions(float centerX, float centerY) { - ArrayList<TargetDrawable> chevrons = mChevronDrawables; - final int size = chevrons.size(); - for (int i = 0; i < size; i++) { - TargetDrawable target = chevrons.get(i); - if (target != null) { - target.setPositionX(centerX); - target.setPositionY(centerY); - } - } - } - - private void hideChevrons() { - ArrayList<TargetDrawable> chevrons = mChevronDrawables; - final int size = chevrons.size(); - for (int i = 0; i < size; i++) { - TargetDrawable chevron = chevrons.get(i); - if (chevron != null) { - chevron.setAlpha(0.0f); - } - } - } - - @Override - protected void onDraw(Canvas canvas) { - mOuterRing.draw(canvas); - final int ntargets = mTargetDrawables.size(); - for (int i = 0; i < ntargets; i++) { - TargetDrawable target = mTargetDrawables.get(i); - if (target != null) { - target.draw(canvas); - } - } - final int nchevrons = mChevronDrawables.size(); - for (int i = 0; i < nchevrons; i++) { - TargetDrawable chevron = mChevronDrawables.get(i); - if (chevron != null) { - chevron.draw(canvas); - } - } - mHandleDrawable.draw(canvas); - } - - public void setOnTriggerListener(OnTriggerListener listener) { - mOnTriggerListener = listener; - } - - private float square(float d) { - return d * d; - } - - private float dist2(float dx, float dy) { - return dx*dx + dy*dy; - } - - private float getScaledTapRadiusSquared() { - final float scaledTapRadius; - if (AccessibilityManager.getInstance(mContext).isEnabled()) { - scaledTapRadius = TAP_RADIUS_SCALE_ACCESSIBILITY_ENABLED * mTapRadius; - } else { - scaledTapRadius = mTapRadius; - } - return square(scaledTapRadius); - } - - private void announceTargets() { - StringBuilder utterance = new StringBuilder(); - final int targetCount = mTargetDrawables.size(); - for (int i = 0; i < targetCount; i++) { - String targetDescription = getTargetDescription(i); - String directionDescription = getDirectionDescription(i); - if (!TextUtils.isEmpty(targetDescription) - && !TextUtils.isEmpty(directionDescription)) { - String text = String.format(directionDescription, targetDescription); - utterance.append(text); - } - if (utterance.length() > 0) { - announceText(utterance.toString()); - } - } - } - - private void announceText(String text) { - setContentDescription(text); - sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); - setContentDescription(null); - } - - private String getTargetDescription(int index) { - if (mTargetDescriptions == null || mTargetDescriptions.isEmpty()) { - mTargetDescriptions = loadDescriptions(mTargetDescriptionsResourceId); - if (mTargetDrawables.size() != mTargetDescriptions.size()) { - Log.w(TAG, "The number of target drawables must be" - + " euqal to the number of target descriptions."); - return null; - } - } - return mTargetDescriptions.get(index); - } - - private String getDirectionDescription(int index) { - if (mDirectionDescriptions == null || mDirectionDescriptions.isEmpty()) { - mDirectionDescriptions = loadDescriptions(mDirectionDescriptionsResourceId); - if (mTargetDrawables.size() != mDirectionDescriptions.size()) { - Log.w(TAG, "The number of target drawables must be" - + " euqal to the number of direction descriptions."); - return null; - } - } - return mDirectionDescriptions.get(index); - } - - private ArrayList<String> loadDescriptions(int resourceId) { - TypedArray array = getContext().getResources().obtainTypedArray(resourceId); - final int count = array.length(); - ArrayList<String> targetContentDescriptions = new ArrayList<String>(count); - for (int i = 0; i < count; i++) { - String contentDescription = array.getString(i); - targetContentDescriptions.add(contentDescription); - } - array.recycle(); - return targetContentDescriptions; - } - - public int getResourceIdForTarget(int index) { - final TargetDrawable drawable = mTargetDrawables.get(index); - return drawable == null ? 0 : drawable.getResourceId(); - } - - public void setEnableTarget(int resourceId, boolean enabled) { - for (int i = 0; i < mTargetDrawables.size(); i++) { - final TargetDrawable target = mTargetDrawables.get(i); - if (target.getResourceId() == resourceId) { - target.setEnabled(enabled); - break; // should never be more than one match - } - } - } - - /** - * Gets the position of a target in the array that matches the given resource. - * @param resourceId - * @return the index or -1 if not found - */ - public int getTargetPosition(int resourceId) { - for (int i = 0; i < mTargetDrawables.size(); i++) { - final TargetDrawable target = mTargetDrawables.get(i); - if (target.getResourceId() == resourceId) { - return i; // should never be more than one match - } - } - return -1; - } - - private boolean replaceTargetDrawables(Resources res, int existingResourceId, - int newResourceId) { - if (existingResourceId == 0 || newResourceId == 0) { - return false; - } - - boolean result = false; - final ArrayList<TargetDrawable> drawables = mTargetDrawables; - final int size = drawables.size(); - for (int i = 0; i < size; i++) { - final TargetDrawable target = drawables.get(i); - if (target != null && target.getResourceId() == existingResourceId) { - target.setDrawable(res, newResourceId); - result = true; - } - } - - if (result) { - requestLayout(); // in case any given drawable's size changes - } - - return result; - } - - /** - * Searches the given package for a resource to use to replace the Drawable on the - * target with the given resource id - * @param component of the .apk that contains the resource - * @param name of the metadata in the .apk - * @param existingResId the resource id of the target to search for - * @return true if found in the given package and replaced at least one target Drawables - */ - public boolean replaceTargetDrawablesIfPresent(ComponentName component, String name, - int existingResId) { - if (existingResId == 0) return false; - - try { - PackageManager packageManager = mContext.getPackageManager(); - // Look for the search icon specified in the activity meta-data - Bundle metaData = packageManager.getActivityInfo( - component, PackageManager.GET_META_DATA).metaData; - if (metaData != null) { - int iconResId = metaData.getInt(name); - if (iconResId != 0) { - Resources res = packageManager.getResourcesForActivity(component); - return replaceTargetDrawables(res, existingResId, iconResId); - } - } - } catch (NameNotFoundException e) { - Log.w(TAG, "Failed to swap drawable; " - + component.flattenToShortString() + " not found", e); - } catch (Resources.NotFoundException nfe) { - Log.w(TAG, "Failed to swap drawable from " - + component.flattenToShortString(), nfe); - } - return false; - } -} diff --git a/core/java/com/android/internal/widget/multiwaveview/TargetDrawable.java b/core/java/com/android/internal/widget/multiwaveview/TargetDrawable.java index 16bec16..5a4c441 100644 --- a/core/java/com/android/internal/widget/multiwaveview/TargetDrawable.java +++ b/core/java/com/android/internal/widget/multiwaveview/TargetDrawable.java @@ -18,7 +18,6 @@ package com.android.internal.widget.multiwaveview; import android.content.res.Resources; import android.graphics.Canvas; -import android.graphics.ColorFilter; import android.graphics.drawable.Drawable; import android.graphics.drawable.StateListDrawable; import android.util.Log; diff --git a/core/java/com/android/server/AppWidgetBackupBridge.java b/core/java/com/android/server/AppWidgetBackupBridge.java new file mode 100644 index 0000000..2ea2f79 --- /dev/null +++ b/core/java/com/android/server/AppWidgetBackupBridge.java @@ -0,0 +1,63 @@ +/* + * 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.server; + +import java.util.List; + +/** + * Runtime bridge between the Backup Manager Service and the App Widget Service, + * since those two modules are intentionally decoupled for modularity. + * + * @hide + */ +public class AppWidgetBackupBridge { + private static WidgetBackupProvider sAppWidgetService; + + public static void register(WidgetBackupProvider instance) { + sAppWidgetService = instance; + } + + public static List<String> getWidgetParticipants(int userId) { + return (sAppWidgetService != null) + ? sAppWidgetService.getWidgetParticipants(userId) + : null; + } + + public static byte[] getWidgetState(String packageName, int userId) { + return (sAppWidgetService != null) + ? sAppWidgetService.getWidgetState(packageName, userId) + : null; + } + + public static void restoreStarting(int userId) { + if (sAppWidgetService != null) { + sAppWidgetService.restoreStarting(userId); + } + } + + public static void restoreWidgetState(String packageName, byte[] restoredState, int userId) { + if (sAppWidgetService != null) { + sAppWidgetService.restoreWidgetState(packageName, restoredState, userId); + } + } + + public static void restoreFinished(int userId) { + if (sAppWidgetService != null) { + sAppWidgetService.restoreFinished(userId); + } + } +} diff --git a/core/java/com/android/server/SystemService.java b/core/java/com/android/server/SystemService.java index e374563..bf36bb1 100644 --- a/core/java/com/android/server/SystemService.java +++ b/core/java/com/android/server/SystemService.java @@ -123,6 +123,38 @@ public abstract class SystemService { public void onBootPhase(int phase) {} /** + * Called when a new user is starting, for system services to initialize any per-user + * state they maintain for running users. + * @param userHandle The identifier of the user. + */ + public void onStartUser(int userHandle) {} + + /** + * Called when switching to a different foreground user, for system services that have + * special behavior for whichever user is currently in the foreground. This is called + * before any application processes are aware of the new user. + * @param userHandle The identifier of the user. + */ + public void onSwitchUser(int userHandle) {} + + /** + * Called when an existing user is stopping, for system services to finalize any per-user + * state they maintain for running users. This is called prior to sending the SHUTDOWN + * broadcast to the user; it is a good place to stop making use of any resources of that + * user (such as binding to a service running in the user). + * @param userHandle The identifier of the user. + */ + public void onStopUser(int userHandle) {} + + /** + * Called when an existing user is stopping, for system services to finalize any per-user + * state they maintain for running users. This is called after all application process + * teardown of the user is complete. + * @param userHandle The identifier of the user. + */ + public void onCleanupUser(int userHandle) {} + + /** * Publish the service so it is accessible to other services and apps. */ protected final void publishBinderService(String name, IBinder service) { diff --git a/core/java/com/android/server/SystemServiceManager.java b/core/java/com/android/server/SystemServiceManager.java index eb8df0e..87a50a9 100644 --- a/core/java/com/android/server/SystemServiceManager.java +++ b/core/java/com/android/server/SystemServiceManager.java @@ -131,6 +131,58 @@ public class SystemServiceManager { } } + public void startUser(final int userHandle) { + final int serviceLen = mServices.size(); + for (int i = 0; i < serviceLen; i++) { + final SystemService service = mServices.get(i); + try { + service.onStartUser(userHandle); + } catch (Exception ex) { + Slog.wtf(TAG, "Failure reporting start of user " + userHandle + + " to service " + service.getClass().getName(), ex); + } + } + } + + public void switchUser(final int userHandle) { + final int serviceLen = mServices.size(); + for (int i = 0; i < serviceLen; i++) { + final SystemService service = mServices.get(i); + try { + service.onSwitchUser(userHandle); + } catch (Exception ex) { + Slog.wtf(TAG, "Failure reporting switch of user " + userHandle + + " to service " + service.getClass().getName(), ex); + } + } + } + + public void stopUser(final int userHandle) { + final int serviceLen = mServices.size(); + for (int i = 0; i < serviceLen; i++) { + final SystemService service = mServices.get(i); + try { + service.onStopUser(userHandle); + } catch (Exception ex) { + Slog.wtf(TAG, "Failure reporting stop of user " + userHandle + + " to service " + service.getClass().getName(), ex); + } + } + } + + public void cleanupUser(final int userHandle) { + final int serviceLen = mServices.size(); + for (int i = 0; i < serviceLen; i++) { + final SystemService service = mServices.get(i); + try { + service.onCleanupUser(userHandle); + } catch (Exception ex) { + Slog.wtf(TAG, "Failure reporting cleanup of user " + userHandle + + " to service " + service.getClass().getName(), ex); + } + } + } + /** Sets the safe mode flag for services to query. */ public void setSafeMode(boolean safeMode) { mSafeMode = safeMode; diff --git a/core/java/com/android/server/WidgetBackupProvider.java b/core/java/com/android/server/WidgetBackupProvider.java new file mode 100644 index 0000000..a2efbdd --- /dev/null +++ b/core/java/com/android/server/WidgetBackupProvider.java @@ -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 com.android.server; + +import java.util.List; + +/** + * Shim to allow core/backup to communicate with the app widget service + * about various important events without needing to be able to see the + * implementation of the service. + * + * @hide + */ +public interface WidgetBackupProvider { + public List<String> getWidgetParticipants(int userId); + public byte[] getWidgetState(String packageName, int userId); + public void restoreStarting(int userId); + public void restoreWidgetState(String packageName, byte[] restoredState, int userId); + public void restoreFinished(int userId); +} diff --git a/core/java/com/android/server/net/BaseNetworkObserver.java b/core/java/com/android/server/net/BaseNetworkObserver.java index 5502a17..430dd63 100644 --- a/core/java/com/android/server/net/BaseNetworkObserver.java +++ b/core/java/com/android/server/net/BaseNetworkObserver.java @@ -57,7 +57,7 @@ public class BaseNetworkObserver extends INetworkManagementEventObserver.Stub { } @Override - public void interfaceClassDataActivityChanged(String label, boolean active) { + public void interfaceClassDataActivityChanged(String label, boolean active, long tsNanos) { // default no-op } |